What is monkey patching and why is it so abhorrent? - monkeypatching

Is a monkey patch when you extend a class?
class Hash
def delete_blanks!
delete_if { |k, v| v.is_nil? }
end
end
Then you can do this:
h = { red: 'stop', yellow: 'ready', purple: nil, green: 'go'}
h.delete_blanks! #=> { red: 'stop', yellow: 'ready', green: 'go' }
Is that a monkey patch? What about this:
class ActiveRecord::Base
def foo
"bar"
end
end
What's so bad about that?
I'm not being argumentative, I'm ready to assume it is bad, but how should I go about emulating this behaviour without a monkey patch? Should I send the method?

I wouldn't say monkey patching is bad, it's more like a "should avoid" practice, there are definitely scenarios where monkey patching is useful.
Another way to solve this problem is through inheritance so you could have something like:
class SuperHash < Hash
def delete_blanks!
delete_if { |k, v| v.is_nil? }
end
end

Related

Nested JSON to Ruby class (with validation)

I have the following JSON:
{
"ordernumber":"216300001000",
"datecreated":"2016-11-08T14:23:06.631Z",
"shippingmethod":"Delivery",
...
"customer":{
"firstname":"Victoria",
"lastname":"Validator"
},
"products":[
{
"sku":"ABC1",
"price":"9.99"
},
...
]
}
With the corresponding Ruby classes including validators:
class Task
include ActiveModel::Model
include ActiveModel::Serializers::JSON
validates ..., presence: true
...
end
class Product
include ActiveModel::Model
include ActiveModel::Serializers::JSON
validates ..., presence: true
...
end
class Customer
include ActiveModel::Model
include ActiveModel::Serializers::JSON
validates ..., presence: true
...
end
What I want to do is serialise the JSON to a Ruby class. The problem is that the Task class get's initialised correctly. But the nested classes like Customer and Product remain hashes. (A Task has one Customer and multiple Products)
Example:
json = %Q{{ "ordernumber":"216300001000", "datecreated":"2016-11-08T14:23:06.631Z", "shippingmethod":"Delivery", "customer":{ "firstname":"Victoria", "lastname":"Validator" }, "products":[ { "sku":"ABC1", "price":"9.99" } ] }}
task = Task.new()
task.from_json(json)
task.class
# => Task
task.products[0].class
# => Hash
How do I do this using ActiveModel and also validate the nested JSON? (I'm not using Rails)
As far as I know, ActiveModel::Model brings validations and other handy stuff, but it does not bring tools to handle association problems like this one. You have to implement his behavior yourself.
First of all, I'd use the builtin initialization system that ActiveModel::Model provides. Then I'd define products= and customer= to take the attributes and initialize instances of the proper classes. And call the validations of the associated records.
class Task
include ActiveModel::Model
attr_reader :products, :customer
# ...
validate :associated_records_are_valid
def products=(ary)
#products = ary.map(&Product.method(:new))
end
def customer=(attrs)
#customer = Customer.new(attrs)
end
private
def associated_records_are_valid
products.all?(&:valid?) && customer.valid?
end
end
attributes = JSON.parse(json_str)
task = Task.new(attributes)
Look at this topic: Is it possible to convert a JSON string to an object?. I am not in front of a computer right now to post a code, but I think that answer solves your problem.

How do you Skip an Object in an ActiveModel Serializer Array?

I have searched through all the active model serializer (v 0.9.0) documentation and SO questions I can find, but can't figure this out.
I have objects which can be marked as "published" or "draft". When they aren't published, only the user who created the object should be able to see it. I can obviously set permissions for "show" in my controller, but I also want to remove these objects from the json my "index" action returns unless it is the correct user. Is there a way to remove this object from the json returned by the serializer completely?
In my activemodel serializer, I am able to user filter(keys) and overloaded attributes to remove the data, as shown using my code below, but I can't just delete the entire object (I'm left having to return an empty {} in my json, trying to return nil breaks the serializer).
I'm probably missing something simple. Any help would be much appreciated!
class CompleteExampleSerializer < ExampleSerializer
attributes :id, :title
has_many :children
def attributes
data = super
(object.published? || object.user == scope || scope.admin?) ? data : {}
end
def filter(keys)
keys = super
(object.published? || object.user == scope || scope.admin?) ? keys : {}
end
end
That looks correct, try returning an array instead of a hash when you dont want any keys. Also, I don't think calling super is necessary b/c the filter takes in the keys.
Also, I don't think defining an attributes method is necessary.
I have chapters that can either be published or unpublished. They're owned by a story so I ended doing something like below.
has_many :unpublished_chapters, -> { where published: false }, :class_name => "Chapter", dependent: :destroy
has_many :published_chapters, -> { where published: true }, :class_name => "Chapter", dependent: :destroy
Inside of my serializer, I choose to include unpublished_chapters only if the current_user is the owner of those chapters. In ams 0.8.0 the syntax is like so.
def include_associations!
include! :published_chapters if ::Authorization::Story.include_published_chapters?(current_user,object,#options)
include! :unpublished_chapters if ::Authorization::Story.include_unpublished_chapters?(current_user,object,#options)
end
In my case, it's not so bad to differentiate the two and it saves me the trouble of dealing with it on the client. Our situations are similar but say you want to get all of the chapters by visiting the chapters index route. This doesn't make much sense in my app but you could go to that controller and render a query on that table.

Ruby on Rails: Accessing HTML elements from model

I'm trying to find a way to access html elements from my view from within my model.
I'm trying to access the the page title. On my view I have this:
<% provide(:title, 'Baseline') %>
And from my model, here is my latest attempt:
def steps
if #title == 'Baseline'
%w[sfmfa phq whoqol_bref positive_negative ]
elsif #title == 'Treatment Completion'
%w[smfa phq ]
else
%w[]
end
end
I also tried by using params[:title], but params isn't recognized in the model. This feels like a really dumb question, but I haven't been able to find a straight forward answer.
Any input would be greatly appreciated.
EDIT. Adding more detail.
As mentioned below, I'm going about this wrong. So now I'm trying to pass the correct identifier from my controller, to the model.
Currently I have pagination for one page, 'Baseline'. I'm trying to allow for pagination on 2 other pages. I simply need to be able to change the value of whats held in steps.
My old 'steps' method looked like this:
subject.rb
def steps
%w[sfmfa ...]
end
And here are what steps is used for:
def current_step
#current_step || steps.first
end
def next_step
self.current_step = steps[steps.index(current_step)+1]
end
def previous_step
self.current_step = steps[steps.index(current_step)-1]
end
def first_step?
current_step == steps.first
end
def last_step?
current_step == steps.last
end
So, I guess my new method might look something like this, where I pass the argument from the controller:
def steps(title)
if title == 'Baseline'
%w[sfmfa ...]
elsif title == 'Other'
%w[sma ...]
else
#Shouldnt get here
end
Also, here is how my view renders the steps:
<%= render "base_#{#subject.current_step}", :f => f %>

Nested strong parameters in rails - AssociationTypeMismatch MYMODEL expected, got ActionController::Parameters()

I'm rendering a model and it's children Books in JSON like so:
{"id":2,"complete":false,"private":false, "books" [{ "id":2,"name":"Some Book"},.....
I then come to update this model by passing the same JSON back to my controller and I get the following error:
ActiveRecord::AssociationTypeMismatch (Book (#2245089560) expected, got ActionController::Parameters(#2153445460))
In my controller I'm using the following to update:
#project.update_attributes!(project_params)
private
def project_params
params.permit(:id, { books: [:id] } )
end
No matter which attributes I whitelist in permit I can't seem to save the child model.
Am I missing something obvious?
Update - another example:
Controller:
def create
#model = Model.new(model_params)
end
def model_params
params.fetch(:model, {}).permit(:child_model => [:name, :other])
end
Request:
post 'api.address/model', :model => { :child_model => { :name => "some name" } }
Model:
accepts_nested_attributes_for :child_model
Error:
expected ChildModel, got ActionController::Parameters
Tried this method to no avail: http://www.rubyexperiments.com/using-strong-parameters-with-nested-forms/
Are you using accepts_nested_attributes_for :books on your project model? If so, instead of "books", the key should be "books_attributes".
def project_params
params.permit(:id, :complete, :false, :private, books_attributes: [:id, :name])
end
I'm using Angular.js & Rails & Rails serializer, and this worked for me:
Model:
has_many :features
accepts_nested_attributes_for :features
ModelSerializer:
has_many :features, root: :features_attributes
Controller:
params.permit features_attributes: [:id, :enabled]
AngularJS:
ng-repeat="feature in model.features_attributes track by feature.id
My solution to this using ember.js was setting the books_attributes mannualy.
In controller:
def project_params
params[:project][:books_attributes] = params[:project][:books_or_whatever_name_relationships_have] if params[:project][:books_or_whatever_name_relationships_have]
params.require(:project).permit(:attr1, :attr2,...., books_attributes: [:book_attr1, :book_attr2, ....])
end
So rails checks and filters the nested attributes as it expected them to come
This worked for me. My parent model was an Artist and the child model was a Url.
class ArtistsController < ApplicationController
def update
artist = Artist.find(params[:id].to_i)
artist.update_attributes(artist_params)
render json: artist
end
private
def artist_params
remap_urls(params.permit(:name, :description, urls: [:id, :url, :title, :_destroy]))
end
def remap_urls(hash)
urls = hash[:urls]
return hash unless urls
hash.reject{|k,v| k == 'urls' }.merge(:urls_attributes => urls)
end
end
class Artist < ActiveRecord::Base
has_many :urls, dependent: :destroy
accepts_nested_attributes_for :urls, allow_destroy: true
end
class Url < ActiveRecord::Base
belongs_to :artist
end
... and in coffeescript (to handle deletions):
#ArtistCtrl = ($scope, $routeParams, $location, API) ->
$scope.destroyUrls = []
$scope.update = (artist) ->
artist.urls.push({id: id, _destroy: true}) for id in $scope.destroyUrls
artist.$update(redirectToShow, artistError)
$scope.deleteURL = (artist,url) ->
artist.urls.splice(artist.urls.indexOf(url),1)
$scope.destroyUrls.push(url.id)
Something is missing from all of the answers, which is the inputs for fields_for in the form.
The form works if you do this:
f.fields_for #model.submodel do ..
However, the form is sent as model[submodel], but that's what causes the error others have mentioned in their answers. If you try to do model.update(model_params), Rails will raise an error that it's expecting a Submodel type.
To fix this, make sure you follow the :name, value format:
f.fields_for :submodel, #model.submodel do ...
Then in the controller, make sure you put _attributes on your params:
def model_params
params.require(:model).permit(submodel_attributes: [:field])
end
Now the save, update, etc. will work fine.
Wasted several days trying to figure out how to use accepts_nested_attributes with Angular, and the issue is always the same: Rails whitelist will not allow the variables into the params hash. I've tried every single different whitelisting syntax that everyone said on SO and other blogs, tried using :inverse, tried using habtm and mas_many_through, tried manually rolling my own solution but that wont work if the whitelist wont allow params through, tried doing what http://guides.rubyonrails.org says about 'Outside the Scope of Strong Parameters', tried removing whitelisting all together which isnt really an option but it causes other problems anyways. Not sure why rails 4 strong parameter whitelisting wont allow arbitrary data thru, thats a huge problem especially if accepts_nested_attributes doesn't work either.... I guess we are left to just create/delete all associations on a separate page/form/controller and look like an idiot making my end users use several forms/pages to do something that should be easily doable on 1 page with 1 form. Ya know, usually I expect Angular to screw me, but this time Angular worked quite well and it was actually Rails 4 that screwed me twice on 1 issue that should be very straightforward.

Renaming a single element in a select dropdown

In a Rails app, I've got an array that is a list of entry types: ["steps", "calories", "water", "sodium", "sugar", "fruits_veggies"]
In my view, I'm creating a select box to choose one of the above entry types: .controls= f.select :type, entry_type_options
This works just fine, but I'd like to replace "fruits_veggies" with "fruits & veggies" in the dropdown box. How can I do this for a single value? options_for_select looked promising, but I'm not sure what route to take.
Note that I'm using a helper for "entry_type_options":
def entry_type_options
#entry_type_options ||= Entry::TYPES.map {|t| [t.capitalize, t] }
end
This is definitely a case of being up too late. I just changed the helper:
def entry_type_options
#entry_type_options ||= Entry::TYPES.map {|t| (t == "fruits_veggies") ? ["Fruits & Veggies", t] : [t.capitalize, t]}
end