Ruby on Rails : conditional display - ruby-on-rails

I have just started with Ruby on Rails and I have a use case where I need to map my Json response from a call back into an existing Ruby model. There are a few fields, however, that cannot be mapped directly. I have a resource class looking like this:
class SomeClassResource
attr_reader :field
def initialize(attrs = {})
#field = attrs['some_other_field']
end
I have then a method that if some_other_field matches to a specific string then return true, alternatively false, as follows:
def some_method(value)
value == 'aString' ? true : false
end
I then need to display either true or false in my view. What is the correct approach?
Thank you

First off, you should simplify your method to this:
# app/models/some_class_resource.rb
def some_method(value)
value == 'aString'
end
Then to show it in a view you would want to get the value in a controller first (into an instance variable scoped to the view):
# app/controllers/some_class_resources_controller.rb
class SomeClassResourcesController << ApplicationController
def show
resource = SomeClassResource.new
#true_or_false = resource.some_value('aString')
end
end
Then you'll want a view with something like this:
# app/views/some_class_resources/show.html.erb
<h1>My view</h1>
Was it true? <%= #true_or_false %>
You will also need the appropriate show route in your routes file.
# config/routes.rb
resources :some_class_resources, only: [:show]

Related

How can I access a method in a controller that is defined in a model?

I'm pretty new to rails, and the project I'm working on requires me to access an existing method. However, it's written in a model, and I'm not sure how I can get it to run in the controller my API needs to hit
I've tried routing to the method in the model, but learned I can't do that. From what I've gathered, this is sort of the way it will work, right?
model.rb
def method_i_need
//code
end
controller.rb
def method_to_call_other_method
//code
end
At the risk of stealing #sergio's points...
If your method is defined inside Model.rb, then both the following will work in your controller:
def method_to_call_other_method
Model.first.method_i_need
end
def method_to_call_other_method
Model.find(params[:id]).method_i_need
end
As the commentor said, you just need an instance of your model (Model.first or Model.find(params[:id])) and to then call the method you defined in your model, on the instance of the model. And the params[:id] is obviously dependent on what params you're getting through.
Any instance of a class will have the public instance methods available to be called on the instance object. It's very common to instantiate model class instances within a controller action.
Here's an example elaborating on previous answer and comments how you can do this in Rails.
class Person < ActiveRecord::Base
def say_hello
language == 'DE' ? 'Guten Tag' : 'Hello'
end
end
class PersonsController < ApplicationController
def random_person
#random_person = Person.find(Person.pluck(:id).sample)
# you can now call #random_person.say_hello
end
def person_greetings
# this examples assumes we only have 2 languages, EN and DE
languages = Person.pluck(:language).uniq.sort
#greetings = languages.each_with_object({}) do |language, hash|
hash[language] = Person.new(language: language).say_hello
end
end
end
# #greetings should return the following hash
=> {
"DE" => "Guten Tag",
"EN" => "Hello"
}
Likewise, class methods can also be called directly when needed inside a controller action method for example in model you may have a class method defined like this inside the Person model.
def self.languages
pluck(:language).uniq.sort
end
This method can be called from any controller or other classes where appropriate for example:
def languages
#people_count = Person.count # active record method to get number of people in database
#languages = Person.languages
end
Where you might use this inside of a controller action's view
<div>
There are <%= #people_count %> total people in the system.
Of them, <%= #languages.count %> languages are spoken.
Those include the following:
<ol>
<% #languages.each do |language| %>
<li><%= language %></li>
</ol>
</div>

How can I pass in a variable defined in a class into a Rails form?

If I have a controller
class MyController < ApplicationController
vals = [...]
def new
...
end
def create
if save
...
else
render 'new'
end
end
how can I make the "vals" variable accessible to both methods? In my "new" view I want to use the "vals" variable for a drop-down menu, but rails is giving me errors. Of course, I could just copy the variable twice, but this solution is inelegant.
As Sebastion mentions a before_ hook / callback is one way to go about it, however as you mentioned it is for a dropdown menu, I am guessing it is a non-changing list, if so I would suggest perhaps using a Constant to define the values, perhaps in the model they are specific to, or if it is to be used in many places a PORO would do nicely to keep things DRY. This will then also allow you to easily access it anywhere, for example in models for a validation check, or to set the options of the dropdown menu in the view, or in the controller if you so wish:
class ExampleModel
DROPDOWN_VALUES = [...].freeze
validates :some_attr, inclusion: { in: DROPDOWN_VALUES }
end
class SomeController < ApplicationController
def new
# can call ExampleModel::DROPDOWN_VALUES here
end
def create
# also here, anywhere actually
end
end
You could use a before_* callback, e.g a before_action, this way you sets your vals variable as an instance one and make it to be available for your both new and create methods, something like:
class SomeController < ApplicationController
before_action :set_vals, only: [:new, :create]
def new
...
# #vals is available here
end
def create
if save
...
# and here
else
render 'new'
end
end
private
def set_vals
#vals = [...]
end
end
A different way from the ones before (although probably just having the instance method is preferred as in Sebastian's solution) is, take advantage of the fact that functions and local variables are called in the same way in ruby and just write:
def vals
#vals ||= [...]
end
and you should be able to access it on the controllers (not the views). If you want it on your views as well you can call at the beginning of the controller
helper_method :vals
If you want to be able to modify vals using vals="some value"
def vals= vals_value
#vals = vals_value
end
Take into account that probably using the intance variable as in Sebastian's solution is preferred, but if you, for whatever reason, are settled on being able to call "vals" instead of "#vals" on the view (for example if you are using send or try), then this should be able to do it for you.
Define in corresponding model
Eg :
class User < ActiveRecord::Base
TYPES = %w{ type1 type2 type3 }
end
and use in ur form like
User::TYPES
=> ["type1", "type2", "type3"]
You can reuse this anywhere in the application.

Check for unused parameters in a Rails controller's action's params

Is it possible to check for parameters passed in Rails' params that haven't been used in an action?
For example if params had values for :foo_id and :bar_id, then I'd want something like
class MyController < ApplicationController
def show
#foo = Foo.find(params[:foo_id])
Rails.logger.info params.unused_parameters # => [:bar_id]
end
end
In your case, you can simply use except method in 'Hash`:
unused_parameters = params.except(:foo_id)
unused_parameter_keys = unused_parameters.keys
And unused_parameter_keys is the result you want

How to modify (encode and decode) routes id, but without changes in the model or controller?

I want to expose my database ids and encode/decode the id with routes helper. For encoding I use Hashids gem.
Now I have:
routes.rb
get 'companies/:id/:year', to: 'company#show', as: 'companies'
company url:
/companies/1/2015
For id encoding I have encode/decode helper methods:
def encode(id)
# encode...
return 'ABC123'
end
def decode(hashid)
# decode...
return 1
end
How I can implemented, that id will be with routes helper converted?
So must show the URL:
/companies/ABC123/2015
and controller must get automatically params with id 1.
Thanks for your answers! But I wont to decode params id without changes in the model or controller. After long consideration, I have decided the params id to manipulate, before controller get params. I manipulate params in routes Constraints.
example helper:
encoding_helper.rb
module EncodingHelper
def encode(id)
# encode...
return 'ABC123'
end
def decode(hashid)
# decode...
return 1
end
end
Create path with a encode id:
companies_path(id: encode(1), year: 2015) # => /companies/ABC123/2015
Manipulate params in routes Constraints:
lib/Constraints/decode_company_id.rb
module Constraints
class DecodeId
extend EncodingHelper
def self.matches?(request)
request.params['id'] = decode(request.params['id']).to_s if request.params['id'].present?
true
end
end
end
config/routes.rb
constraints(Constraints::DecodeId) do
get 'companies/:id/:year', to: 'company#show', as: 'companies'
end
After decode params id with constraints und without manipulation in controller, params id is 1.
You can use the to_param method for this.
#in Company
def to_param
self.encoded_id
end
def encoded_id
self.class.encode_id(self.id)
end
def find_by_encoded_id(encoded_id)
self.find_by_id(self.class.decode_id(encoded_id)
end
#class methods
class << self
def encode_id(id)
#encoding algorithm here
end
def decode_id(encoded_id)
#decoding algorithm here
end
end
this will mean that urls featuring the id of the company will actually use the encoded_id instead, assuming you pass through the company object to a path helper, eg company_path(#company).
Then, in your companies controller, you just need to make sure that you find_by_encoded_id(params[:id]) rather than find_by_id(params[:id]).
Rails router shouldn't be doing any decoding:
The Rails router recognizes URLs and dispatches them to a controller's action.
The logic should belong to the controller.
When your controller receives an encoded response:
#Appropriate controller
def show
Company.decode(params[:id])
end
This work work nicely if you slightly adjust your model method to:
def self.decode(code)
# decode => get id
find(id) #returns Company object
end
you can try this. custom method of friendly id
in model
extend FriendlyId
friendly_id :decode
# Try building a slug based on the following fields in
# increasing order of specificity.
def decode
conditional_check(self.id)
end
private
def conditional_check(id)
return "ABC123" if id == 1
end

Dynamic namespaced controllers w/ fallback in Rails

I have a somewhat bizarre requirement for a new Rails application. I need to build an application in which all routes are defined in multiple namespaces (let me explain). I want to have an application in which school subjects (math, english, etc) are the namespaces:
%w[math english].each do |subject|
namespace subject.to_sym do
resources :students
end
end
This is great and it works but it requires me to create a namespaced StudentsController for each subject which means if I add a new subject then I need to create a new controller.
What I would like is to create a Base::StudentsController and if, let's say the Math::StudentsController exists then it will be used and if it doesn't exist, then we can dynamically create this controller and inherit from Base::StudentsController.
Is this something that is possible? If so then how would I go about implementing this?
With routes defined this way:
%w[math english].each do |subject|
scope "/#{subject}" do
begin
"#{subject.camelcase}::StudentsController".constantize
resources :students, controller: "#{subject}::students", only: :index
rescue
resources :students, controller: "base::students", only: :index
end
end
end
rake routes outputs:
students GET /math/students(.:format) base::students#index
GET /english/students(.:format) english::students#index
if english/students_controller.rb is present and math/students_controller. is absent.
To restate your requirements:
Minimal declarations per subject/resource pair
Use dedicated controller (Math::StudentsController) if it exists, otherwise use base controller (StudentsController)
Rails expects each route to have a dedicated controller, and doesn't really have a good way to support the second requirement. So, this is how I would do it:
Dynamicroutes::Application.routes.draw do
SUBJECTS = [ "math", "english", "chemistry" ]
RESOURCES = [ "assignments", "students" ]
class DedicatedSubjectResourceControllerConstraint
def initialize(subject, resource)
#subject = subject
#resource = resource
end
def matches?(request)
begin
defined?("#{#subject.capitalize}::#{#resource.capitalize}")
return true
rescue NameError
Rails.logger.debug "No such class: #{#subject.capitalize}::#{#resource.capitalize}"
return false
end
end
end
class ValidSubjectConstraint
def matches?(request)
return SUBJECTS.include?(request.path_parameters[:subject])
end
end
SUBJECTS.each do |subject|
RESOURCES.each do |resource|
namespace subject, :constraints => DedicatedSubjectResourceControllerConstraint.new(subject, resource) do
resources resource
end
end
end
RESOURCES.each do |resource|
scope "/:subject", :constraints => ValidSubjectConstraint.new do
resources resource
end
end
end
This sounds like a use for const_missing. If what you want to do is
to create a Base::StudentsController
and if, let's say the Math::StudentsController exists
then it will be used
and if it doesn't exist, then we can dynamically create this controller and inherit from Base::StudentsController
You can achieve that by adding dynamic constant lookup (const_missing) and dynamic constant definition with inheritance (Object.const_set).
I imagine something like this; with a few tweaks and more rigorous checking, would work:
# initializers/dynamic_controllers.rb
class ActionDispatch::Routing::RouteSet
SUBJECTS = [ "math", "english", "chemistry" ]
def const_missing(name, *args, &block)
if SUBJECTS.any?{ |subject| name.include? subject.uppercase }
Object.const_set name, Class.new(Base::StudentsController)
else
super
end
end
end
That'll add dynamic constant lookups to ActionDispatch::Routing::RouteSet, from which Dynamicroutes::Application.routes inherits, so undefined constants in Dynamicroutes::Application.routes.draw will generate the corresponding classes subclassed from Base::StudentsController.
I believe this will do it:
%w[math english].each do |subject|
namespace subject.to_sym do
resources :students
end
end
match ':subject/students(/:action(/:id))' => 'base/students'
With these combined routes, /math/students goes to the Math::StudentsController, /english/students/ goes to the English::StudentsController, and all other subjects (e.g. /physics/students and /cs/students) go to the Base::StudentsController.
Which I think is exactly what you want and only adds one line of code to your original solution.
All the routing helpers like resources, scope, etc are just functions inside your application's routes. You could just define a custom function as follows:
YourApplication.routes.draw do
# Let's define a custom method that you're going to use for your specific needs
def resources_with_fallback(*args, &block)
target_module = #scope[:module].camelize.constantize
target_controller = "#{args.first.to_s}_controller".camelize
fallback_controller = args.last.delete(:fallback).to_s.camelize.constantize
# Create the target controller class
# using fallback_controller as the superclass
# if it doesn't exist
unless target_module.const_defined?(target_controller)
target_module.const_set target_controller, Class.new(fallback_controller)
end
# Call original resources method
resources *args, &block
end
# Now go ahead and define your routes!
namespace "test" do
namespace "new" do
# Use our custom_resources function and pass a fallback parameter
custom_resources :photos, :fallback => 'base/some_controller'
end
end
end
I tested this in Rails 3.2, but it should equally work well in all 3.x versions.
I included no null checks or begin/rescue blocks anywhere. Since you're going to use this custom function only when required, I'm assuming that you will pass the correct and necessary parameters. If say you passed a fallback controller that doesn't exist, I'd rather that the routes parsing fail with an exception, rather than trying to handle it.
Edit: Typo in function arguments
Edit 2: Forgot &block in function arguments
Edit 3: Add "_controller" to the target_controller variable
I ended up writing some custom logic into ActionDispatch::Routing::RouteSet::Dispatcher.controller_reference. I attempt to look up all of the constants required for the given controller and create them if they're missing. This code is FAR from perfect so please feel free to edit w/ improvements.
class ActionDispatch::Routing::RouteSet::Dispatcher
private
def controller_reference(controller_param)
const_name = #controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"
obj = Object
const_name.split('::').each do |cn|
begin
obj = obj.const_get(cn)
rescue
if obj == Object
obj = obj.const_set(cn, Class.new(ApplicationController))
else
puts "Creating #{obj}::#{cn} based on Generic::#{cn}"
obj = obj.const_set(cn, Class.new("Generic::#{cn}".constantize))
end
end
end
ActiveSupport::Dependencies.constantize(const_name)
end
end

Resources