I've got a rails app. I'm trying to finetune different models and controllers. I can make the things work, but I'm not sure if I'm doing the preferred way. So I have an event.rb for my own in-app fullcalendar where I can CRUD new events and have social.rb for omniauth authorization (google calendar in this case). I'm trying to display the gcal events in my fullcalendar, so based on the social.rb data (tokens, keys) I make an API call to google to get gcal event times and then display the data in events/index page (fullcalendar).
Here are two of my methods I don't know how/where to put in my app. My questions (I have 3 as they are closely connected):
Method types. As I see init_google_api_calendar_client should be a class method if I put it into the social.rb (I'm not even sure if I should place it there). For the get_busy_events I simply can't decide what type to use.
I guess I should put these method in a model/module, but which one (social/event/something else)?
How should I invoke the methods?
events_controller
def index
#user = current_user
#google = #user.socials.where(provider: "google_oauth2").first
#client = Social.init_google_api_calendar_client(#google) ### Is this the proper way to call this method?
#get_busy_times = #####how to call the get_buys_events method?
#event = Event.new
#events = Event.allevents(current_user)
respond_to do |format|
format.html
format.json { render json: #events }
format.js
end
end
social.rb
def self.init_google_api_calendar_client(google_account)
#method only called if google_oauth2 social exists
client = Google::APIClient.new
client.authorization.access_token = google_account.token
client.authorization.client_id = ENV['GOOGLE_API_KEY']
client.authorization.client_secret = ENV['GOOGLE_API_SECRET']
client.authorization.refresh_token = google_account.refresh_token
return client
end
Where to put this method? Should be class/instance? How should be invoked?
def get_busy_events(client)
service = client.discovered_api('calendar', 'v3')
result = client.execute(
api_method: service.freebusy.query,
body_object: { timeMin: start_at,
timeMax: end_at,
items: items},
headers: {'Content-Type' => 'application/json'})
end
You're asking a good question and in my experience there isn't a universally adopted convention in the Rails community for where to put code that makes server-side API calls to 3rd party services.
When your app uses external APIs in ways that are okay to be asynchronous, it would be appropriate to invoke these calls using ActiveJob or gems like sidekiq directly. Here's a decent write-up (see the section labeled "Talking with external APIs"): https://blog.codeship.com/how-to-use-rails-active-job/
But, if you are building an app that truly dependent on the 3rd-party API and asynchronous calls don't provide you much benefit, then I suggest defining a different type of class that is neither a model, view, nor controller. A common practice to follow when there aren't already gems to meet your needs is to write a ruby wrapper class for the API saved under the lib folder. Keep your model methods scoped towards logic that requires database interactivity. Invoke model methods from your controller as well as webservice methods defined in your wrapper class.
Good luck!
Related
I try to stick to principles of thin controller and thin model. And I believe that business logic related code should be in the service classes.
I write a Rails back-end application accepting JSON requests. And I need to validate, that the one parameter is present. Let's assume that I have:
class UserController
def change_status
user = User.find(params[:id])
render json: UserStatusChanger.new(user, params[:status]).perform!
end
end
class UserStatusChanger
attr_reader :user, :status
def initialize(user, status)
#user = user
#status = status
end
def perform!
# complex logic here
{result: 'ok'}
end
end
And now let's suppose that I need to receive non-blank params[:status]. Sure, in the real world it is much more complex with more parameters. :)
My question is: Where should I put validation of params[:status]?
My thoughts are:
If I put it in the controller, I need an integration test to test the validation. But there are some good looking solutions, as rails_params gem. But I can face with problem of big controller method, having many validations. And also in the unit test my service will work wrong if some input parameter is nil without validation before performing complex logic.
If I put is in the service, the test will be more lightweight. But I should catch exceptions via ApplicationController#rescue_from and this will be not tested.
I think, that you should do it in controller. As for me, i usually do it in before_action method. According to MVC pattern, all params and routing logic must be in controller.
I am building a sample rails 4 app and I'm unclear about something. I want to access an external API to pull data on sports news via an ajax call.
So for example if you have a list of teams in the teams#index view, when you click on one team a widget will get populated with the latest results / scores for that team-- the results info is provided by an external API service, not the local database.
Do I need to create a controller for this service to allow the rails ajax request to have a local endpoint? Should the actual request mechanism happen in this controller? or would it be better to build a helper for the data request and call that from the controller?
On the other hand it's possible to do it all via javascript in the browser.
Thanks-- I realize there's a dozen ways to do things in rails, I'm just unclear on the "right" way to handle this type of situation.
I tend to do this with a helper module that you can unit test independently. To give you a similar, trivial example, here's a module that you could use to wrap the Gravatar API:
# /lib/gravatar.rb
module Gravatar
def self.exists email
url = self.image_url email
url = url + '?d=404'
response = HTTParty.get url
return response.code != 404
end
def self.image_url email, size=nil
gravatar_id = self.gravatar_id email
size_url = size ? '?s=' + size.to_s : ''
"http://gravatar.com/avatar/#{gravatar_id}.png" + size_url
end
def self.gravatar_id email
Digest::MD5::hexdigest(email.downcase)
end
end
Then, you can make a call to Gravatar::image_url as necessary. If you wanted to be able to access a Gravatar image via an ajax call, you could simply wrap it in a controller:
# /app/controllers/api/users_controller.rb
class Api::UsersController < Api::BaseController
def gravatar_for_user_id
user = User.find_by_id(params[:id])
render plain: Gravatar::image_url user.email, :status => 200
end
end
This model can be applied to whatever external APIs you need to hit, and modularizing your interface will always make unit testing more straightforward.
When using the Rails method, find_or_create_by, does it belong in the model or the controller? I am having a hard time understanding how to actually implement this method.
I want my rails application to accept JSON messages from users. The users will be sending data back to the server so it can be saved in the database. That being said, I would assume the user would have to use the 'POST' or 'PATCH method to store or update the data on my server. When I look at my routes the 'POST' method is used by the create action.
I have read the following Rails documentation but it didn't clarify anything to me. http://guides.rubyonrails.org/active_record_querying.html#find-or-create-by
Would I place the find_or_create_by method in my create action like so? Or does it belong somewhere else? It doesn't seem right in the create action...
class WifiNetworksController < ApplicationController
def create
#wifi_network = WifiNetwork.find_or_create_by(bssid: params[:bssid],
ssid: params[:ssid],
channel: params[:channel], etc...)
end
end
Ultimately I want:
Users to save new networks via JSON if it doesn't exist
Users to update existing networks via JSON if certain attributes have improved (like signal strength)
Thank you for your time!
Final Update - Thanks for the great advice everyone! I had to take a bit of everybody's advice to get it to work! Below is what I ended up doing.. Seems to work well with no errors.
def create
respond_to do |format|
if #wifi_network = WifiNetwork.find_by(bssid: wifi_network_params[:bssid])
# Logic for checking whether to update the record or not
#wifi_network.update_attributes(wifi_network_params) if #wifi_network.rssi < params[:rssi]
format.json { render :nothing => true }
else
# Must be a new wifi network, create it
#wifi_network = WifiNetwork.create(wifi_network_params)
format.json { render :nothing => true }
end
end
end
If you use strong params you can do this in your controller:
def create
#wifi_network = WifiNetwork.find_or_create_by(bssid: wifi_network_params[:bssid])
#wifi_network.update_attributes(wifi_network_params)
end
Then when a user makes a curl like:
curl -X POST localhost:3000/wifi_networks -d "wifi_network[bssid]=bssid1&wifi_network[ssid]=ssid1&wifi_network[channel]=channel1"
your create action will look up a WifiNetwork by it's bssid and update the ssid and channel attributes, or if it doesn't exist it will create a WifiNetwork with the bssid param and then update the newly created record with the rest of the atts. Be careful because if the wifi_network_params for the other attrs are empty they will update the params to nil.
I think it may be good to take a step back and really think about the interface of your application. Is there any particular reason why you need to use find_or_create_by and do everything in one controller action?
Why not simplify things and adhere to REST by having separate 'create' and 'update' actions on your WifiNetworksController:
class WifiNetworksController < ApplicationController
def create
#wifi_network = WifiNetwork.new(wifi_network_params)
if #wifi_network.save
# success response
else
# failure response
end
end
def update
# params[:id] won't work here if the client sending the request doesn't know the id of the
# wifi network, so replace it with the attribute you expect to be able to
# uniquely identify a WifiNetwork with.
if #wifi_network = WifiNetwork.find(params[:id])
# Logic for deciding whether to update or not
#wifi_network.update_attributes(wifi_network_params) if #wifi_network.signal_strength < params[:signal_strength]
else
# wifi_network not found, respond accordingly
end
end
private
# strong_parameters for Rails 4
def wifi_network_params
params.require(:wifi_network).permit(:ssid, :channel,...)
end
end
You could then have validations on your WifiNetwork model to ensure that certain attributes are unique, in order to avoid duplicates.
Or, if you really wanted to, you could combine both create and update into a single action, but create probably isn't the best name semantically.
EDIT: After your comment gave some background info, there probably isn't any benefit to using find_or_create_by, since you won't be able to tell if the record returned was 'created' or 'retrieved', which would allow you to avoid redundant update operations on it.
Assuming the bssid attribute is always a unique parameter:
def create
if #wifi_network = WifiNetwork.find(params[:bssid])
# Logic for checking whether to update the record or not
#wifi_network.update_attributes(wifi_network_params) if #wifi_network.signal_strength < params[:signal_strength]
else
# Must be a new wifi network, create it
#wifi_network = WifiNetwork.create(wifi_network_params)
end
end
I'm currently trying to implement simple audit for users (just for destroy method). This way I know if the user has been deleted by an admin or user deleted itself. I wanted to add deleted_by_id column to my model.
I was thinking to use before_destroy, and to retrieve the user info like described in this post :
http://www.zorched.net/2007/05/29/making-session-data-available-to-models-in-ruby-on-rails/
module UserInfo
def current_user
Thread.current[:user]
end
def self.current_user=(user)
Thread.current[:user] = user
end
end
But this article is from 2007, I'm not sure will this work in multithreaded and is there something more up to date on this topic, has anyone done something like this lately to pass on the experience?
Using that technique would certainly work, but will violate the principle that wants the Model unaware of the controller state.
If you need to know who is responsible for a deletion, the correct approach is to pass such information as parameter.
Instead of using callbacks and threads (both represents unnecessary complexity in this case) simply define a new method in your model
class User
def delete_user(actor)
self.deleted_by_id = actor.id
# do what you need to do with the record
# such as .destroy or whatever
end
end
Then in your controller simply call
#user.delete_user(current_user)
This approach:
respects the MVC pattern
can be easily tested in isolation with minimal dependencies (it's a model method)
expose a custom API instead of coupling your app to ActiveRecord API
You can use paranoia gem to make soft deletes. And then I suggest destroying users through some kind of service. Check, really basic example below:
class UserDestroyService
def initialize(user, destroyer)
#user = user
#destroyer = destroyer
end
def perform
#user.deleted_by_id = #destroyer.id
#user.destroy
end
end
UserDestroyService.new(user, current_user).perform
My rails app produces XML when I load /reports/generate_report.
On a separate page, I want to read this XML into a variable and save it to the database.
How can I do this? Can I somehow stream the response from the /reports/generate_report.xml URI into a variable? Or is there a better way to do it since the XML is produced by the same web app?
Here is my generate_report action:
class ReportsController < ApplicationController
def generate_report
respond_to do |format|
#products = Product.all
format.xml { render :layout => false }
end
end
end
Here is the action I am trying to write:
class AnotherController < ApplicationController
def archive_current
#output = # get XML output produced by /reports/generate_report
# save #output to the database
respond_to do |format|
format.html # inform the user of success or failure
end
end
end
Solved: My solution (thanks to Mladen Jablanović):
#output = render_to_string(:file => 'reports/generate_report.xml.builder')
I used the following code in a model class to accomplish the same task since render_to_string is (idiotically) a protected method of ActionController::Base:
av = ActionView::Base.new(Rails::Configuration.new.view_path)
#output = av.render(:file => "reports/generate_report.xml.builder")
Perhaps you could extract your XML rendering logic to a separate method within the same controller (probably a private one), which would render the XML to a string using render_to_string, and call it both from generate_report and archive_current actions.
What I typically do in this type of situation is to create a separate module/class/model to generate the report (it could even potentially be right in the Product model). This separate component could be in app/models or it could be in lib. In any case, once you have it extracted you can use it anywhere you need it. The controller can call it directly. You can generate it from the console. You can have a cron job generate it. This is not only more flexible, but it also can help smooth out your request response times if the report becomes slow to generate.
Since you are using a template it's understandable that the controller route is convenient, but even if you have to include some kind of ruby templating system in your auxiliary lib, it's still probably going to be less hassle and more flexible then trying to go through the controller.
#output = Product.all.to_xml
I'm sorry, is you question about Xml or about sessions? I mean is the fact that your action generates Xml material to the question? Or do you just want to save the output of the action for latter use?
You said on a "separate" page - you mean on another request? (like after user approved it?)
Why do you want to save the output? Because it should be saved exactly as rendered? (for example user can get frustrated if he clicked to save one report and you saved another)
Or is this thing expensive to generate?
Or may be, I got it wrong and it's about refactoring?