I have a an json Api who received parameters to create a Device, like name, imei, etc. The Device can have one Blacklist object (has_one :blacklist). I would like to know what's the proper-way to create the blacklist object if a params is present in the post request of Device.
Exemple curl -X POST -d api_key=000000 -d device[name]='stack' -d device[blacklist]='true' https://www.example.com/api/devices.json
In the code for the moment I should have
def create
#device = Device.new
#device.update_attributes(strong_parameters)
if params[:device]['blacklist'] && params[:device]['blacklist'] == true
#blacklist = Blacklist.new(device_id: #device.id)
end
render :device, status: 201 # will render with jbuilder #device and #blacklist
end
But I don't like it that much :
Too much logic in one controller
Verifying parameters inside is a good practice?
If no parameters are given, how to handle the request? I know that strong parameters should return a 400, but what about #device I just created.
This controller smells for me.
Feedbacks welcome.
The result when doing a PATCH
class DevicesController
before_action :found_device, only: :blacklist # get `#device`
before_action :blacklist_device, only: :blacklist
def blacklist
render :device, status: 200
end
private
def blacklist_device
if (params[:device]['blacklisted'] and
params[:device]['blacklisted'] == true and
#blacklist = BlacklistedDevice.create(device_id: #device.id, organisation_id: current_store.organisation.id))
#device.reload
else
render json: { error: "Missing or incorrect 'blacklisted' parameter" }, status: :unprocessable_entity
end
end
end
Too much logic in the conrtoller ? No
I have also heard a lot 'too much logic in the controller is bad' but this is bullshit or rather I believe the words are not accurate enough.
What that phrase means for me, is that for example, model validations should not be in the controller, and the controller should remain light for very basic REST actions. Controller should only be a bridge between the HTML request and the model. Think of it this way : you may have several controllers modifying the same model. What you would write in EVERY controller, should most likely instead be written in the model as a validation.
But here you're dealing with specific requests (transforming a device[blacklist] == true as a Blacklist Model isn't something "natural", so yes in my opinion it should be in the controller.
Plus, a controller action of just 6 lines isn't what we could call "too much logic"
Verifying parameters inside is good Practice ? Yes/No
I assume by that you mean writing specific lines of codes in the controller like if params[xxx] == blabla or something equivalent
The way you did was good. You use specific code only for the special parameter (the blacklist) and the rest of the params go into the model as strong params, so the model validations will do the rest.
Verify parameters only if it's relevant to this particular controller (for example, if it was site-based, you could probably use a different implementation of the blacklist so the difference would have to be in the controller.
If no parameters are given, how to handle the request? I know that strong parameters should return a 400, but what about #device I just created.
This the part I don't quite like about your current implementation. You don't check for the success of your save operations. Here's what you could have written (check the result of every persistence operation result, and render appropriately)
def create
#device = Device.new
if #device.update_attributes(strong_parameters)
if (params[:device]['blacklist']
and params[:device]['blacklist'] == true
and #blacklist = Blacklist.create(device_id: #device.id))
# Handle stuff when everything is cool
render :device, status: 201 # will render with jbuilder #device and
else
# Handle stuff when there's no blacklist param true
end
else
# Handle error on model save
end
end
Inspecting params is well put in the controller - that's it's purpose - the model layer should not have knowledge of request parameters.
But you can put this info in a transient attribute with
class Device
attr_accessor 'create_blacklisted'
end
Then you can create an input field for that new attribute and an after_initialize callback in the Device model as well that can subsequently create the Blacklist entry.
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 have ruby on rails app and my controller should process request which creates many objects. Objects data is passed from client via json using POST method.
Example of my request (log from controller):
Processing by PersonsController#save_all as JSON
Parameters: {"_json"=>[{"date"=>"9/15/2014", "name"=>"John"},
{"date"=>"9/15/2014", "name"=>"Mike"}], "person"=>{}}
So i need to save these two users but i have some issues:
How to verify strong parameters here? Only Name and Date attributes can be passed from client
How can I convert String to Date if i use Person.new(params)?
Can i somehow preprocess my json? For example i want to replace name="Mike" to name="Mike User" and only then pass it in my model
I want to enrich params of every person by adding some default parameters, for example, i want to add status="new_created" to person params
First of all I'd name the root param something like "users", then it gives a structure that is all connected to the controller name and the data being sent.
Regarding strong params. The config depends of your rails app version. <= 3.x doesn't have this included so you need to add the gem. If you're on >= 4.x then this is already part of rails.
Next in your controller you need to define a method that will do the filtering of the params you need. I should look something like:
class PeopleController < ApplicationController
def some_action
# Here you can call a service that receives people_params and takes
# care of the creation.
if PeopleService.new(people_params).perform
# some logic
else
# some logic
end
end
private
def base_people_params
params.permit(people: [:name, :date])
end
# Usually if you don't want to manipulate the params then call the method
# just #people_params
def people_params
base_people_params.merge(people: normalized_params)
end
# In case you decided to manipulate the params then create small methods
# that would that separately. This way you would be able to understand this
# logic when returning to this code in a couple of months.
def normalized_params
return [] unless params[:people]
params[:people].each_with_object([]) do |result, person|
result << {
name: normalize_name(person[:name]),
date: normalize_date(person[:date]),
}
end
end
def normalize_date(date)
Time.parse(date)
end
def normalize_name(name)
"#{name} - User"
end
end
If you see that the code starts to get to customized take into a service. It will help to help to keep you controller thin (and healthy).
When you create one reason at the time (and not a batch like here) the code is a bit simpler, you work with hashes instead of arrays... but it's all pretty much the same.
EDIT:
If you don't need to manipulate a specific param then just don't
def normalized_params
return [] unless params[:people]
params[:people].each_with_object([]) do |result, person|
result << {
name: person[:name],
date: normalize_date(person[:date]),
}
end
end
I am working on a Rails API backend with a separate Rails/Angular front-end codebase. The responses from the Rails API must be structured in a certain way to match with the front-end flash messages. A(n) (slightly boiled down) example controller response is
render json: {status: "Unauthorized", code: "AUTH.UNAUTHORIZED", fallback_msg: "Unauthorized request"}
so basically all my controllers are full of this, and sometimes when there are 5 possible responses for a method (ex: if a user resets their email the response can be invalid password, invalid email, email is already registered etc). A coworker suggested abstracting these methods out into the model, so the model is response for sending back these messages and then the controller can just have
def ctrl_method
user = User.update(password: password)
render json: user, status(user)
end
(where status is another method that provides the HTTP status code based on the object's status attribute)
My question is is this best practice? I understand Rails MVC and feel like the responsibility of sending the json message belongs to the controller not the model.
I say you're both right. The controller should have the responsibility of sending the message, and the methods should be abstracted out--just not into the model, more like a class that lives in /lib.
This should make testing easier as well.
If you want to deal with ActiveRecord errors I think you use errors.full_messages and use the same code and status for such errors (status: 'Forbidden', code: '').
Note that you should customize your messages in locale files see guide. But it's useful if you want to translate your app in different languages.
Success messages can be inferred from the controller and the action (see below).
class ApplicationController < ActionController::Base
...
def render_errors(object)
render json: {status: 'Forbidden', code: 'WRONG_DATA', fallback_msg: object.errors.full_messages.join(", ")}
end
def render_success(message = nil)
message ||= I18n.t("#{self.controller_name}.message.#{self.action_name}"
render json: {status: 'OK', code: 'OK', fallback_msg: message}
end
end
class SomeController < ApplicationController
def update
if #some.update(params)
render_success
else
render_errors(#some)
end
end
end
Without knowing any more details, I would think about utilizing concerns to deal with the statuses. This allows business logic to be encapsulated without muddying up your models. So you could have
module Concerns
module Statuses
def deal_with_show
# business logic goes here to generate the status
end
def deal_with_index
# business logic goes here to generate the status
end
def deal_with_create
# business logic goes here to generate the status
end
def default_status
# set a default status here
end
end
end
and then in the controllers
class MyController < ApplicationController
include Concerns::Statuses
def index
render json: deal_with_index
end
end
Of course, that breakdown of statuses in the concern is arbitrary. It can be handled however makes sense: by method, by verb, or by some other distinction.
This is a good bit on concerns.
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 have decimal field in my DB. Users can input values in two formats: with comma or point (11,11 or 11.11).
But MySQL allows to save data only in 'point' format, so i want to process data before saving with regex like this:
sub(/,/,".")
How can i do it in Rails3?
If I understand you correctly, this could be done in the controller or the model. I might use the before_save callback in the model to achieve this in the following way:
class Item < ActiveRecord::Base
before_save :standardise_numbers
...
protected
# Called before this object is saved to the DB
def standardise_numbers
self.number.sub!(",", ".")
end
end
Where number is the attribute you're wanting to convert.
I assume you don't need to convert it back to comma representation to display to the user? If you do, you may want to look into the internationalisation API for Rails, Il8n. It handles this kind of stuff and more, so definitely worth looking into.
Alternative Solution (edit)
Based on your feedback, my above solution doesn't work since the number is already converted and the decimal part lost when it is passed into the model. A similar piece of code could be used in the controller to intercept and convert the number in the params hash itself:
class PostController < ActionController
before_filter :standardise_numbers, :only => [ :create, :update ]
def create
#post = Post.create(params[:post])
end
protected
# Intercepts the params hash
def standardise_numbers
params[:post][:number].sub!(",", ".")
end
end
This simplifies the create and update methods, allowing you to deal with the hash in the same way you normally would.
I played this it and found this:
Suppose what in form field number, user inputs value '12,13'.
Value from form go to PostController to 'create' method
class PostController < ApplicationController
def create
#post = Post.new(params[:post])
#on this step instance of Post model created, validated and filled with relevant values
#so #post.number == '12' #(decimal), it cuts ',13'
#we need to redefine #post.number
#post.number = params[:post][:number].gsub(/,/,'.').to_f # => 12.13
#and after that save the post
#post.save
end