Rails how to save data upon creation with an external API? - ruby-on-rails

In my app for bike_rental_shops, I'm making it possible for these shops to manage their bike rentals.
Context
Bike rental companies also offer their bikes on website of external parties, therefore I'm connecting my Rails application with these external websites. I'm currently handling this in my controller when a user goes to the index page. Before loading the index page an API call is made to the external rental website and new bike rentals should be saved in the database.
Question
How to save only new rentals and not all rentals linked to a certain external rental website?
Current consideration
The only thing I can come up with is adding a database column with {external_website}_rental_id for a specific external website, so I can match them. However, this would mean that I need to add a seperate rental_id for every external rental website.
Code
rentals_controller.rb
def index
shop = Shop.find(params[:id])
request_rental_api
#bikes = shop.bikes
end
private
def request_rental_api
# set variables
base_url = "https://www.rentalwebsite.com"
url = "/rest/api/rentals"
token = 'TOKEN'
# init connection object
connection = Faraday.new(:url => base_url) do |c|
c.use Faraday::Request::UrlEncoded
c.use Faraday::Response::Logger
c.use FaradayMiddleware::FollowRedirects
c.adapter Faraday::Adapter::NetHttp
end
# send request
response = connection.get url do |request|
request.headers["Authorization"] = token
request.headers["Accept"] = "application/json"
end
bookings = JSON.parse(response.body['results'])
# check if rental is unique, and if so save it.
# Rental.create(????)
end
JSON output API
{
"next": null,
"previous": null,
"results": [
{
"activation_requested": false,
"id": 21664,
"slug": "rental-test"
#more information....
}
}]

you can create 2 columns
provider_rental_id id returned in response JSON
provider name of provider, to which request was made
Then to only create new records
rental_ids = bookings['results'].map { |r| r['id'] }
return if rental_ids.none?
existing_rental_ids = Rental.where(provider_rental_id: rental_ids, provider: 'Name of Provider').pluck(:provider_rental_id)
new_rental_ids = rental_ids - existing_rental_ids
new_rental_ids.each do |rental_id|
Rental.create(
provider: 'Name of Provider',
provider_rental_id: rental_id,
...
or if you are using rails 6 you can check upsert_all
Note: It does not instantiate any models nor does it trigger Active Record callbacks or validations.
Additionally try to move this into a background cronjob

Related

Rails Devise token generator with service objects

I am working on a fairly large Rails app which due to large no of activities uses service objects (& light-services gem). Typically actions are performed in steps in the services, with many of the functions/objects used in these steps instantiated in the model it is working with (Customer.rb) in this case.
In one step I am trying to generate a Devise reset_password_token, but hit a glitch
Customer.rb
def self.generate_token
Devise.token_generator
end
and then in service object reset_password.rb
def generate_token
raw, hashed = Devise.token_generator.generate(Customer, :reset_password_token)
producer.reset_password_token = hashed
producer.reset_password_token_sent_at = Time.now.utc
#token = raw
end
All that goes in to db is the timestamp, and a nil, nil for token although token gets put in to the url endpoint so is being generated
If anyone encountered this scenario before, tips more than welcome!
Here is rest of code, not setting the token in DB properly.
def validate_producer_email
self.valid_email = EmailValidator.valid?(producer.email)
end
# #return [TrueClass, FalseClass]
def validate_token
self.producer.reset_password_token = reset_password_token, params['reset_password_token']
customer = Customer.producer.find_by(reset_password_token: reset_password_token)
# true if customer.email && Devise.secure_compare(reset_password_token, params[:reset_password_token])
customer.present?
end
def update_producer
return if ask_underwriter?
Producer::Update
.with(self)
.run(
producer: producer,
force: current_administrator.blank?,
params: {
customer: {
reset_password_token: reset_password_token
}
}
)
end
If anyone has any tips on how to fix?
Thanks

How to create dynamic id in elastic search client in Rails?

I am trying to post data elk server using elastic search client.I need to pass dynamic id.Below the code I am using
require 'elasticsearch'
module PostDataToElasticsearchConcern
extend ActiveSupport::Concern
included do
helper_method :post_executions_to_elasticsearch
end
def post_executions_to_elasticsearch(username,mac_address,parametername)
json = {
"mac_address" => mac_address.to_s,
"parameter_name" => parametername.to_s,
"user_name" => username.to_s,
"time" => Time.now.utc.iso8601
}
postdata=json.to_s.gsub!("=>",":")
puts "JSON----------------------------"+postdata.to_s
client = Elasticsearch::Client.new url:"My Server:9200"
response = client.index(index: 'testingapp', body: json, id: '1')
end
end
Now I have passed id as 1. But id needs to be created dynamically.how to create dynamic id?.
Thanks
I am able to create dynamic id with below code.
response = client.index(index: 'testingapp', body: json)
Removing id will created dynamic id.

Consume external API and performance

I am working a Ruby on Rails project that consumes data through some external API.
This API allows me to get a list of cars and display them on my single webpage.
I created a model that holds all methods related to this API.
The controller uses list_cars method from the model to forward the data to the view.
This is the model dedicated to the API calls:
class CarsApi
#base_uri = 'https://api.greatcars.com/v1/'
def self.list_cars
cars = Array.new
response = HTTParty.get(#base_uri + 'cars',
headers: {
'Authorization' => 'Token token=' + ENV['GREATCARS_API_TOKEN'],
'X-Api-Version' => ENV["GREATCARS_API_VERSION"]
})
response["data"].each_with_index do |(key, value), index|
id = response["data"][index]["id"]
make = response["data"][index]["attributes"]["make"]
store = get_store(id)
location = get_location(id)
model = response["data"][index]["attributes"]["model"]
if response["data"][index]["attributes"]["status"] == "on sale"
cars << Job.new(id, make, store, location, model)
end
end
cars
end
def self.get_store(job_id)
store = ''
response_related_store = HTTParty.get(#base_uri + 'cars/' + job_id + "/relationships/store",
headers: {
'Authorization' => 'Token token=' + ENV['GREATCARS_API_TOKEN'],
'X-Api-Version' => ENV["GREATCARS_API_VERSION"]
})
if response_related_store["data"]
store_id = response_related_store["data"]["id"]
response_store = HTTParty.get(#base_uri + 'stores/' + store_id,
headers: {
'Authorization' => 'Token token=' + ENV['GREATCARS_API_TOKEN'],
'X-Api-Version' => ENV["GREATCARS_API_VERSION"]
})
store = response_store["data"]["attributes"]["name"]
end
store
end
def self.get_location(job_id)
address, city, country, zip, lat, long = ''
response_related_location = HTTParty.get(#base_uri + 'cars/' + job_id + "/relationships/location",
headers: {
'Authorization' => 'Token token=' + ENV['GREATCARS_API_TOKEN'],
'X-Api-Version' => ENV["GREATCARS_API_VERSION"]
})
if response_related_location["data"]
location_id = response_related_location["data"]["id"]
response_location = HTTParty.get(#base_uri + 'locations/' + location_id,
headers: {
'Authorization' => 'Token token=' + ENV['GREATCARS_API_TOKEN'],
'X-Api-Version' => ENV["GREATCARS_API_VERSION"]
})
if response_location["data"]["attributes"]["address"]
address = response_location["data"]["attributes"]["address"]
end
if response_location["data"]["attributes"]["city"]
city = response_location["data"]["attributes"]["city"]
end
if response_location["data"]["attributes"]["country"]
country = response_location["data"]["attributes"]["country"]
end
if response_location["data"]["attributes"]["zip"]
zip = response_location["data"]["attributes"]["zip"]
end
if response_location["data"]["attributes"]["lat"]
lat = response_location["data"]["attributes"]["lat"]
end
if response_location["data"]["attributes"]["long"]
long = response_location["data"]["attributes"]["long"]
end
end
Location.new(address, city, country, zip, lat, long)
end
end
It takes... 1 minute and 10 secondes to load my home page!
I wonder if there is a better way to do this and improve performances.
It takes... 1 minute and 10 seconds to load my home page! I wonder if there is a better way to do this and improve performances.
If you want to improve the performance of some piece of code, the first thing you should always do is add some instrumentation. It seems you already have some metric how long loading the whole page takes but you need to figure out now WHAT is actually taking long. There are many great gems and services out there which can help you.
Services:
https://newrelic.com/
https://www.skylight.io/
https://www.datadoghq.com/
Gems
https://github.com/MiniProfiler/rack-mini-profiler
https://github.com/influxdata/influxdb-rails
Or just add some plain old logging
N+1 request
One assumption why this is slow could be that you do for every car FOUR additional requests to fetch the store and location. This means if you display 10 jobs on your homepage you would need to do 50 API requests. Even if each request just takes ~1 second it's almost one minute.
One simple idea is to cache the additional resources in a lookup table. I'm not sure how many jobs would share the same store and location though so not sure how much it would actually safe.
This could be a simple lookup table like:
class Store
cattr_reader :table do
{}
end
class << self
def self.find(id)
self.class.table[id] ||= fetch_store(id)
end
private
def fetch_store(id)
# copy code here to fetch store from API
end
end
end
This way, if several jobs have the same location / store you only do one request.
Lazy load
This depends on the design of the page but you could lazy load additional information like location and store.
One thing many pages do is to display a placeholder or dominant colour and lazy load further content with Java Script.
Another idea could be load store and location when scrolling or hovering but this depends a bit on the design of your page.
Pagination / Limit
Maybe you're also requesting too many items from the API. See if the API has some options to limit the number of items you request e.g. https://api.greatcars.com/v1/cars/limit=10&page=1
But as said, even if this is limited to 10 items you would end up with 50 requests in total. So until you fix the first issue, this won't have much impact.
General Caching
Generally I think it's not a good idea to always send an API request for each request you page gets. You could introduce some caching to e.g. only do an API request once every x minutes / hour / day.
This could be as simple as just storing in a class variable or using memcached / Redis / Database.

Trying to add custom fields to envelope definition from docusign rails example

I got the embedded signing method I got from rails example on how to implement docusign embedded signing into your rails app.
I added a custom_fields object and added to the envelope object I created from the example
def embedded_signing
# base_url is the url of this application. Eg http://localhost:3000
base_url = request.base_url
user = HiringManager.find params[:hiring_manager_id]
# Fill in these constants
# Obtain an OAuth token from https://developers.hqtest.tst/oauth-token-generator
access_token = Token.access_token
# Obtain your accountId from demo.docusign.com -- the account id is shown in the drop down on the
# upper right corner of the screen by your picture or the default picture.
account_id = ENV["docusign_client_id"]
# Recipient Information:
signer_name = user.full_name
signer_email = user.email
base_path = 'http://demo.docusign.net/restapi'
client_user_id = user.id # Used to indicate that the signer will use an embedded
# Signing Ceremony. Represents the signer's userId within
# your application.
authentication_method = 'None' # How is this application authenticating
# the signer? See the `authenticationMethod' definition
file_name = 'agreement.pdf' # The document to be signed.
# Step 1. Create the envelope request definition
envelope_definition = DocuSign_eSign::EnvelopeDefinition.new
envelope_definition.email_subject = "Please sign this Newcraft Placement Agreement"
doc = DocuSign_eSign::Document.new({
:documentBase64 => Base64.encode64(File.binread(File.join('data', file_name))),
:name => "Agreement signed", :fileExtension => "pdf", :documentId => "1"})
# The order in the docs array determines the order in the envelope
envelope_definition.documents = [doc]
# create a signer recipient to sign the document, identified by name and email
# We're setting the parameters via the object creation
signer = DocuSign_eSign::Signer.new ({
:email => signer_email, :name => signer_name,
:clientUserId => client_user_id, :recipientId => 1
})
sign_here = DocuSign_eSign::SignHere.new ({
:documentId => '1', :pageNumber => '4',
:recipientId => '1', :tabLabel => 'SignHereTab',
:xPosition => '75', :yPosition => '70'
})
# Tabs are set per recipient / signer
tabs = DocuSign_eSign::Tabs.new({:signHereTabs => [sign_here]})
signer.tabs = tabs
# Add the recipients to the envelope object
recipients = DocuSign_eSign::Recipients.new({:signers => [signer]})
envelope_definition.recipients = recipients
# Add custom fields to the envelope object
custom_fields = DocuSign_eSign::CustomFieldV2.new({
:configuration_type => 'text', :required => 'true',
:name => 'date', :fieldId => '', :value => 'Todays date'
})
envelope_definition.custom_fields = custom_fields
# Request that the envelope be sent by setting |status| to "sent".
# To request that the envelope be created as a draft, set to "created"
envelope_definition.status = "sent"
# Step 2. Call DocuSign with the envelope definition to have the
# envelope created and sent
configuration = DocuSign_eSign::Configuration.new
configuration.host = base_path
api_client = DocuSign_eSign::ApiClient.new configuration
api_client.default_headers["Authorization"] = "Bearer " + access_token
envelopes_api = DocuSign_eSign::EnvelopesApi.new api_client
results = envelopes_api.create_envelope account_id, envelope_definition
envelope_id = results.envelope_id
# Step 3. create the recipient view request for the Signing Ceremony
view_request = DocuSign_eSign::RecipientViewRequest.new
# Set the url where you want the recipient to go once they are done signing
# should typically be a callback route somewhere in your app.
view_request.return_url = "https://juice.newcraft.io/edit-manager"
# How has your app authenticated the user? In addition to your app's
# authentication, you can include authenticate steps from DocuSign.
# Eg, SMS authentication
view_request.authentication_method = authentication_method
# Recipient information must match embedded recipient info
# we used to create the envelope.
view_request.email = signer_email
view_request.user_name = signer_name
view_request.client_user_id = client_user_id
# Step 4. call the CreateRecipientView API
results = envelopes_api.create_recipient_view account_id, envelope_id, view_request
user.signed_agreement = true
user.save
# Step 5. Redirect the user to the Signing Ceremony
# Don't use an iFrame!
# State can be stored/recovered using the framework's session or a
# query parameter on the returnUrl (see the makeRecipientViewRequest method)
render json: results
rescue DocuSign_eSign::ApiError => e
#error_msg = e.response_body
render json: #error_msg
end
I am finding it difficult understanding how to insert a custom field that user can manually fill on the pdf agreement document that is displayed for users signature. I also know I need to add the position the custom field tab will reside which the documentation does not really explain how to add to an envelop object you create from a method.
First, let's try to see if we understand your requirement. You want user to fill in some data on the envelope and then collect this data in your application after the envelope is complete, is that correct?
To do that, you don't need custom fields. You can easily to that with regular tabs. Text tabs are probably the simplest way to do so. You add a text tab to your envelope, similar to how you added a SignHere tab and the user would have to fill in the text/value. You can then get this information using other API calls.
Here is the API call to obtain the tab value:
https://developers.docusign.com/esign-rest-api/reference/Envelopes/EnvelopeRecipientTabs/
You basically do a GET /v2.1/accounts/{accountId}/envelopes/{envelopeId}/recipients/{recipientId}/tabs if you are using the v2 or V2.1 API (just replace 2.1 with 2)

Rails ActiveModelSerializer, combine two lists of same-type-models into one serialized response, with different names

I have a rails api, in which I'm using Active Model Serializers (version 0.10.6) to serializer json responses.
I have two arrays, both are of type "FeatureRequest" in an endpoint that returns a list of requests a user has made, and a second list of requests that a user is tagged in. Ideally, I'd like to serialize the response to look something like this:
{
"my_requests" : {
...each serialized request...
},
"tagged_requests" : {
...each serialized request, using the same serializer"
}
}
Is there some way to do this?
Here's my relevant controller method:
def index_for_user
user = User.includes(leagues: :feature_requests).find(params[:user_id])
# Find all requests that this user created
#users_created_feature_requests = FeatureRequest.requests_for_user(user.id)
#feature_requests_in_tagged_leagues = []
user.leagues.each do |league|
#feature_requests_in_tagged_leagues << league.feature_requests
end
...some serialized response here
end
In this code, the two lists are #users_created_feature_requests, and #feature_requests_in_tagged_leagues
Thanks!
Assuming you have a serializer like FeatureRequestSerializer, you can achieve that in the following way:
def index_for_user
user = ...
users_created_feature_requests = ...
feature_requests_in_tagged_leagues = user.leagues.map(&:feature_requests)
# serialized response
render json: {
my_requests: serialized_resource(users_created_feature_requests)
tagged_requests: serialized_resource(feature_requests_in_tagged_leagues)
}
end
private
def serialized_resource(collection, adapter = :attributes)
ActiveModelSerializers::SerializableResource.new(collection,
each_serializer: FeatureRequestSerializer,
adapter: adapter
).as_json
end

Resources