Rails, Model Relationship Question - ruby-on-rails

I have the following which works great:
#request_thread = current_user.request_threads.new(params[:request_thread])
And I have this which works great:
#requestable = find_requestable
#request_thread = #requestable.request_threads.new(params[:request_thread])
But if I try the 2nd line with:
#request_thread = #requestable.current_user.request_threads.new(params[:request_thread])
With the current_user I get the following error:
NoMethodError (undefined method `current_user' for #<Photo:0x10f95c828>):
app/controllers/request_threads_controller.rb:52:in `create'
app/middleware/flash_session_cookie_middleware.rb:14:in `call'
What did I mess up on here?
Thanks
UPDATE - with find_requestable
# http://asciicasts.com/episodes/154-polymorphic-association
def find_requestable
params.each do |name, value|
if name =~ /(.+)_id$/
return $1.classify.constantize.find(value)
end
end
nil
end

The error message tells you exactly whats wrong: current_user doesn't exist for the #requestable object, whatever that is.
current_user is most likely a function inherited from ApplicationController, or at least that's usually where it lives. It usually returns a User object according to the current session. But that isn't a built-in part of Rails, so we need more information if you want me to go into greater detail.
#requestable looks like a polymorphic model instance so it wouldn't be aware of the current session.
There's a bit of Rails magic happening here that I think is confusing you.
Both #requestable.request_threads and current_user.request_threads are helper functions that have been generated by Rails on those objects to save you time by filtering results and filling in values automatically.
I think, by the code you have shown us so far, that you are trying to associate current_user with the new request_thread you are creating. In that case, you can simple merge that into the attributes manually. Since I don't know what your models look like I can only guess at the field names:
#request_thread = #requestable.request_threads.new( params[:request_thread].merge(:user => current_user) )
Like I said, the chainable functions are merely for convenience. For instance, you could write it this way instead:
#request_thread = request_thread.new(params[:request_thread])
#request_thread.requestable = find_requestable
#request_thread.user = current_user

Related

How it works find_by_* in rails

May be its weird for some people about the question. By looking at the syntax its identifiable as class method.
Model.find_by_*
So if its class method it should be defined either in model we created or in
ActiveRecord::Base
So my question is how rails manages to add these methods and makes us available.
Examples like
Model.find_by_id
Model.find_by_name
Model.find_by_status
and etc.
You need to look at ActiveRecord::FinderMethods. Here you can find more details.
Internally, it fires a WHERE query based on attributes present in find_by_attributes. It returns the first matching object.
def find_by_attributes(match, attributes, *args)
conditions = Hash[attributes.map {|a| [a, args[attributes.index(a)]]}]
result = where(conditions).send(match.finder)
if match.bang? && result.nil?
raise RecordNotFound, "Couldn't find #{#klass.name} with #{conditions.to_a.collect {|p| p.join(' = ')}.join(', ')}"
else
yield(result) if block_given?
result
end
end
There is also find_all_by_attributes that returns all matching records.
Rails are using ruby metaprogramming method_missing for that. The method find_by_name is not in a model, instead of this rails are taking name as first argument and it calls it like find_by(name: ?) which is calling where(name: ?).take

Monkeypatch ActiveRecord::FinderMethods

I'm trying to monkey patch ActiveRecord::FinderMethods in order to use hashed ids for my models. So for example User.find(1) becomes User.find("FEW"). Sadly my overwritten method doesn't get called. Any ideas how to overwrite the find_one method?
module ActiveRecord
module FinderMethods
alias_method :orig_find_one, :find_one
def find_one(id)
if id.is_a?(String)
orig_find_one decrypt_id(id)
else
orig_find_one(id)
end
end
end
end
Here's an article that discusses how to actually do what you want by overriding the User.primary_key method like:
class User
self.primary_key = 'hashed_id'
end
Which would allow you to call User.find and pass it the "hashed_id":
http://ruby-journal.com/how-to-override-default-primary-key-id-in-rails/
So, it's possible.
That said, I would recommend against doing that, and instead using something like User.find_by_hashed_id. The only difference is that this method will return nil when a result is not found instead of throwing an ActiveRecord::RecordNotFound exception. You could throw this manually in your controller:
def show
#user = User.find_by_hashed_id(hashed_id)
raise ActiveRecord::RecordNotFound.new if #user.nil?
... continue processing ...
end
Finally, one other note to make this easier on you -- Rails also has a method you can override in your model, to_param, to tell it what property to use when generating routes. By default, of course, it users the id, but you would probably want to use the hashed_id.
class User
def to_param
self.hashed_id
end
end
Now, in your controller, params[:id] will contain the hashed_id instead of the id.
def show
#user = User.find_by_hashed_id(params[:id])
raise ActiveRecord::RecordNotFound.new if #user.nil?
... continue processing ...
end
I agree that you should be careful when doing this, but it is possible.
If you have a method decode_id that converts a hashed ID back to the original id, then the following will work:
In User.rb
# Extend AR find method to allow finding records by an encoded string id:
def self.find(*ids)
return super if ids.length > 1
# Note the short-circuiting || to fall-back to default behavior
find_by(id: decode_id(ids[0])) || super
end
Just make sure that decode_id returns nil if it's passed an invalid hash. This way you can find by Hashed ID and standard ID, so if you had a user with id 12345, then the following:
User.find(12345)
User.find("12345")
User.find(encode_id(12345))
Should all return the same user.

Preferred Workflow for Ruby-YAML interaction

I have just started working on a project where there is a lot of interaction between Ruby and 5-6 levels deep YAML files. Since Ruby will respond with NoMethodError: undefined method '[]' for nil:NilClass when you are trying to access a key that doesn't exist there are lots of methods with the following setup:
def retrieve_som_data(key1, key2)
results = []
if data(key1, key2)
if data_set_2(key, key2)["my_key"]
results = data_set_2(key, key2)["my_other_key"]
end
end
return results.clone
end
This looks horrible, so I am looking at a way to refactor it. I have tried working on a version where I would replace a method like this:
def data(key1, key2)
if data = names_data(key1)
return data[key2]
end
end
with this instead:
def data(key1, key2)
names_data(key1).fetch(key2)
end
This raises a more specific error KeyError which can than be rescued and acted on in any method calling .data(), but this also doesn't seem like a good solution readability wise.
I'd love to get some input on how you are handling situations where you are trying to access YAML_DATA[key][key1][key2][key3][key4] and take into account that any of the provided keys could hit something thats nil.
What are your preferred workflows for this?
If you're using rails, they've added a method, try to Object. For nil objects this will always return nil, rather than throwing so you could do something along the lines of this:
def get_yaml_obj(yaml_data, key1, key2, key3)
yaml_data.try(:[], key1).try(:[], key2).try(:[], key3)
end
Or if you have an arbitary number of keys:
def get_data(yaml_data, keys)
keys.each do |key|
yaml_data = yaml_data.try(:[], key)
end
yaml_data
end

How to transform a string into a variable/field?

I'm new to Ruby and I would like to find out what the best way of doing things is.
Assume the following scenario:
I have a text field where the user can input strings. Based on what the user inputs (after validation) I would like to access different fields of an instance variable.
Example: #zoo is an instance variable. The user inputs "monkey" and I would like to access #zoo.monkey. How can I do that in Ruby?
One idea that crossed my mind is to have a hash:
zoo_hash = { "monkey" => #zoo.monkey, ... }
but I was wondering if there is a better way to do this?
Thanks!
#zoo.attributes gives you a hash of the object attributes. So you can access them like
#zoo.attributes['monkey']
This will give nil if the attribute is not present. Calling a method which doesn't exist will throw NoMethodError
In your controller you could use the public_send (or even send) method like this:
def your_action
#zoo.public_send(params[:your_field])
end
Obviously this is no good, since someone can post somehing like delete_all as the method name, so you must sanitize the value you get from the form. As a simple example:
ALLOWED_METHODS = [:monkey, :tiger]
def your_action
raise unless ALLOWED_METHODS.include?(params[:your_field])
#zoo.public_send(params[:your_field])
end
There is much better way to do this - you should use Object#send or (even better, because it raises error if you try to call private or protected method) Object#public_send, like this:
message = 'monkey'
#zoo.public_send( message )
You could implement method_missing in your class and have it interrogate #zoo for a matching method. Documentation: http://ruby-doc.org/core-1.9.3/BasicObject.html#method-i-method_missing
require 'ostruct' # only necessary for my example
class ZooKeeper
def initialize
#zoo = OpenStruct.new(monkey: 'chimp')
end
def method_missing(method, *args)
if #zoo.respond_to?(method)
return #zoo.send(method)
else
super
end
end
end
keeper = ZooKeeper.new
keeper.monkey #=> "chimp"
keeper.lion #=> NoMethodError: undefined method `lion'

Returning the modified object in rails

I'm trying to make a chainable method such that I can write:
#blog = Blog.new.set_user(current_user).save
instead of
#blog = Blog.new
#blog.user = current_user
I have defined the following method inside the model:
def set_user(user)
self.user = user
return self
end
except that it doesn't work. How do I make a method to return the updated instance so that further chaining can be done upon it?
UPDATE:
My bad, here's what I was doing wrong: The chainable method was named "user" and so it was conflicting with the model's own blog.user method. I changed the name to something unique and voila! it works.
I would try to avoid this in Rails and use the models associations and scopes to do part of the work:
#blog = current_user.blogs.create
About the question you asked, returning self should do the work idd, can you write the output of the console when you create the blog and also let us know what is the output of:
#blog.inspect
Maybe the error is somewhere else...
Your set_user method should return a user instance. self in this context is Blog
def set_user(user)
user
end
If you are using Rails and there is an association between Blog and User you are already able to do
blog.user or blog.users based on what type of association you have in between these models.

Resources