I have the following associations defined in my application:
class Person
belongs_to :type
end
class Type
has_many :people
end
Now, in the new and edit form for person, I need to give a dropdown that will show all the types which will come from the Type model.
Now there are two approach to do the same:
1. make an instance variable in controller for types and access that in the form view.
class PeopleController
before_action :get_types, only: [:new, :create, :edit, :update]
def new
end
def create
end
def edit
end
def update
end
private
def get_types
#types = Type.all
end
end
person/_form.html.erb
...
<%= f.select :type_id, #types.collect{ |type| [type.name, type.id]} %>
...
2. Making a database query in the person_helper
person_helper.rb
module PersonHelper
def get_types
Type.all
end
end
person/_form.html.erb
...
<%= f.select :type_id, get_types.collect{ |type| [type.name, type.id]} %>
...
So, I want to know which is the better approach and why.
Note: As per MVC paradigm, controller will provide the necessary data to the views. Neither I am not able to find any difference in the query execution in both the cases nor, I am able to find any good explanation regarding the same apart from the MVC part.
The idea with the helper is reuse functionalities for example formatting something or extract complicated logic.
So, if that query is something that you will need in many cases, would be a good idea have it in the helper, there is not really an difference in the query execution, is more about use and have a code more clean.
Related
There are already threads on how to pass parameters between methods of the same controller.
But is there a way of passing parameters between methods of different controllers ?
Because sometimes you have for instance an article for which, when you call the #update action on it, you also wish at the same time to update the tags associated with it over the #update action of the TagsController.
The idiom would be something like to instantiate TagsController in ArticlesController#actions and then pass a new instance of Rack::Response with only a :tags part of the whole params-hash to it while doing tags_controller_instance.send(:update).
You would have to send only special parts of the params-hash which the ArticlesController received because TagsController will have different StrongParameters!
I think it boils down to the question about how to create a Rack::Request that copies the Request of ArticlesController on one hand and how to pass to it not the whole params-hash but only meaningful parts of it.
Then it should be possible to ask for the updated taglist afterwards with this.tags in ArticlesController#update, right?
Thanks
Von Spotz
The idiom would be something like to instantiate TagsController in ArticlesController#actions and then pass a new instance of Rack::Response with only a :tags part of the whole params-hash to it while doing tags_controller_instance.send(:update).
Please don't do that! It will be hard to understand & maintain. There might be other side effects you haven't even thought about too.
The question is, what does the TagsController#update do that you don't want to replicate in the ArticlesController? If it's some complex logic, I think you should abstract this e.g. in a (service) object and call it instead.
Something like this:
class UpdateTags
def self.run(params)
new(params).run
end
def initialize(params)
#params = params
end
def run
# copy logic from TagsController
end
end
and then you can use / reuse this service in your controllers
class TagsController
def update
UpdateTags.run(params)
end
end
class ArticlesController
def update
# update Article or move this in a dedicated service too
UpdateTags.run(params)
end
end
Another approach could be to let Article accept attributes for Tags with nested attributes.
Edit
To elaborate a little bit why instantiating another controller is not a good idea.
What about before filters? Is it fine to execute them (again)?
What about view rendering? You obviously don't want to render views so that's additional work and might have side effects?
Other side effects like caching, logging, data analysis.
Instantiating a controller is not a public API so this can change between Rails versions making an update difficult.
It's not a common pattern so it will be hard to understand
I would just stress again that this is not a good idea to do. Duplicated code is better than the wrong abstraction.
This just sounds like a crazy Wile E. Coyote solution to a problem that's trivial to solve with nested attributes.
The only public methods of a controller in Rails should be the actions of the controller which are the methods that respond to HTTP requests and these should only be called though http calls.
Any other method should be private/protected. There is no valid scenario where you would be calling TagsController#update off another controller to update tags as that method should do only one thing - update tags in response to PATCH /tags/:id.
If you want to update an article and its tags in one single request use accepts_nested_attributes_for :tags:
class Article < ApplicationRecord
has_many :tags
accepts_nested_attibutes_for :tags
end
That creates a tags_attributes= setter that you can use update the nested records together with the parent.
And if you want to share functionality between classes where classical inheritance is not suitable use horizontal inheritance. In Ruby this means modules:
module Taggable
private
def tag_attributes
[:foo, :bar, :baz]
end
end
class TagsController < ApplicationController
include Taggable
# PATCH /tags/:id
def update
#tag = Tag.find(params[:id])
if #tag.update(tag_params)
redirect_to #tag, success: 'Tag updated'
else
render :edit
end
end
private
def tag_params
params.require(:tag).permit(*tag_attibutes)
end
end
class ArticlesController < ApplicationController
include Taggable
def update
if #article.update(article_params)
redirect_to #article, success: 'Article updated'
else
render :edit
end
end
private
def article_params
params.require(:article).permit(:title, :body, tags_attributes: tag_attibutes)
end
end
<%= form_with(model: #article) do |f| %>
...
<%= f.fields_for :tags do |tag| %>
<div class="field">
<%= tag.label :foo %>
<%= tag.text_field :foo %>
</div>
<% end %>
<% f.submit %>
<% end %>
In some cases you might choose to use AJAX instead to create/update/delete a nested resource "on-the-fly" by sending asynchronous HTTP requests which do call another controller (but not in the same request). However this is really out of scope for this question.
If you're using params in many controllers, just add them to private methods in the application controller.
Trying to figure our how to set up associations in form.
I have 3 models:
class Request < ActiveRecord::Base
has many :answers
has many :users, through: :answers
end
class Answer < ActiveRecord::Base
belongs to :user
belongs to :request
end
class User < ActiveRecord::Base
has many :answers
has many :requests, through: :answers
end
I am trying to figure out: how to have a User link to Answer#new from Request#Show, and then create an Answer record passing in the Request#Show request_id from the previous page - creating an association between the User's Answer and the Request he was viewing.
My method of doing this now is: I flash the request_id value on Request#Show, and then when a User links to Answer#new, it passes the flashed value into a hidden form tag on Answer#new. This does not seem like the best way to do this.
Any thoughts?
Kudos for the creative approach using flash, however your right there is an easy way. You can pass parameters much between controllers just like passing parameters between methods using the route names.
I didn't quite follow what it was you were trying to achieve in this case but it looks like this blog entry here should get you started..
https://agilewarrior.wordpress.com/2013/08/31/how-to-pass-parameters-as-part-of-the-url-in-rails/
Good luck!
User link to Answer#new from Request#Show
This can be achieved with either sessions or nested resources (or both!). Let me explain:
I would definitely add a nested resource to your requests routes:
#config/routes.rb
resources :requests do
resources :answers, only: [:new, :create] #-> url.com/requests/:request_id/answers [POST]
end
This gives you the ability to call a "nested" route (IE one which sends data to a child controller, and requires "parent" data to be appended to the request).
In your case, you want to create an answer for a request. The most efficient way is to use a routing structure as above; this will allow you to use the following controller method:
#app/controllers/answers_controller.rb
class AnswersController < ApplicationController
def new
#request = Request.find params[:request_id]
#answer = #request.answers.new
end
def create
#request = Request.find params[:request_id]
#answer = #request.answers.new answer_params
#answer.save
end
private
def answer_params
params.require(:answer).permit(:title, :body)
end
end
The above gives you the ability to create an answer by passing the request_id to the nested route. You must note the corresponding route will require a POST method in order to work.
You don't need the new method. If you wanted it, it can easily be handled with the above structure.
Passing the user is a little more tricky.
You can either use the routes, or set a session.
I would personally set a session (it's cleaner):
#app/controllers/requests_controller.rb
class RequestsController < ApplicationController
def show
session[:user_id] = #user.id #-> I don't know how you populate #user
end
end
This will give you the ability to access this session here:
#app/controllers/answers_controller.rb
class AnswersController < ApplicationController
def new
user = User.find session[:user_id]
end
end
#app/views/requests/show.html.erb
<%= link_to "New Answer", request_new_answer_path(request) %>
--
If you're using Devise, the user object should be available in the current_user object (which means you don't have to set session[:user_id]):
#app/controllers/answers_controller.rb
class AnswersController < ApplicationController
def new
## current_user available here if using devise
end
end
To assign a #user to the new answer record, just do this in answers#create:
#app/controllers/answers_controller.rb
class AnswersController < ApplicationController
...
def create
#request = Request.find params[:request_id]
#answer = #request.answers.new answer_params
#answer.user = current_user
#answer.save
end
end
Something like this worked for me:
I have two models (Formula and FormulaMaterial)
Formula has_many FormulaMaterials, which belongs to Formula
My Formula controller sets #formula like so:
#formula = Formula.find(params[:id])
I list my Formula Materials in my Formula show.html.erb by declaring it in my Formula controller like so:
#formula_materials = FormulaMaterial.where(:formula_id => #formula)
When I want to add a new FormulaMaterial to my Formula, the "New Formula Material" button in my show.html.erb file looks like this:
<%= link_to 'Add Material To Formula', new_formula_material_path(:formula_id => #formula), class: "btn btn-success" %>
In the "new_..._path" I set the associated id to the #formula variable. When it passes through to the new.html.erb for my FormulaMaterial, my URL looks like so:
http://localhost:3000/formula_materials/new?formula_id=2
In my FormulaMaterial new.html.erb file, I created a hidden_field that sets the value of the association by using "params" to access the formula_id in the URL like so:
params[:formula_id] %>
I am not sure if this is the best way to do this, but this way has allowed me to pass through the view id from the previous page as a hidden, associated and set field in the form every time.
Hope this helps!
I'm making an online magazine style website and am having difficulties getting the syntax right for my final part of the project. The relationships are working as they should I am just having trouble calling the intended records.
Each post belongs to a category with category_id being the foreign key. When a user clicks this link, <%= link_to 'News', categories_path(:category_id => 1) %>, I'd like for them to be brought to an index page showing only posts with a category_id matching the parameter in the URL.
I've been messing around in the categories_controller.rb for almost two hours now with no luck. Anyone be so kind as to throw this noob a bone?
There are a few components of what you're trying to do. We'll start with the routing side, and make our way to the controller.
First, you need to make the proper routes. Since the post belongs to a category, you will need to have the category id in order to handle performing any sort of operations on the posts. So we'd need a route like /category/:category_id/posts/:id. Luckily, Rails has something to handle this. If you nest a resources within a resources, it'll generate these routes. So, we end up with this:
resources :categories do
resources :posts
end
And that will get you what you want in terms of routes. But now we have to actually implement it. So, we're going to need to take a look at the controllers. If you notice, all of those routes have a :category_id - so looking up the category shouldn't be too difficult:
class PostsController < ApplicationController
before_action :load_category
private
def load_category
#category = Category.find(params[:category_id])
end
end
Now, you have the category loaded, and it shouldn't be too difficult to implement the other methods from there:
class PostsController < ApplicationController
before_action :load_category
def index
#posts = #category.posts
end
def show
#post = #category.posts.find(id: params[:id])
end
# ...
end
In order to reference the Post index path, you'll have to use category_posts_path helper.
Your problem is that you're trying to use an existing route to handle some new functionality (for which it was incidentally not designed). That categories_path route is meant to take you to your category index.
You need to create a method in your controller to perform the functionality you want to see.
class PostsController < ApplicationController
...
def posts_by_category
#posts_by_category = Post.where("category_id = ?", params[:category_id])
end
...
end
Then you're going to need a view to display your #posts_by_category array (I'll leave this exercise to you).
And now for the key to your problem: you need a route pointing to the posts_by_category method.
get 'posts/posts_by_category' => 'posts#posts_by_category'
Now you should be able to create your link with the correct route:
<%= link_to 'News', posts_by_category_path(:category_id => 1) %>
I have two models:
Student
Classroom
Both of them have an action that does the same exact thing: it shows a report of daily activity. That is:
/students/1
/classrooms/1
Grabs activity for the model in question and displays it on the page.
In an attempt to dry this up, I created a ReportsController which extracts all the common logic of building a report.
If I leave the routes like this:
/students/1/report
/classrooms/1/report
Then I can have the ReportsController#show action look for params for :student_id or :classroom_id to determine which model type it is dealing with (for purposes of querying the database and rendering the correct view).
But I would prefer the URLs to be cleaner, so I also changed my routes.rb file to pass the show action for these models to the reports#show controller action:
resources :students, :classrooms do
member do
get :show, to: 'reports#show'
end
end
This works, but I can no longer depend on params to identify which model to work with and which view to render.
Question: should I parse request.fullpath for the model? Or is there a better way to make a shared controller understand which model it is working with?
Routing both show methods to the same controller method for code reuse is somewhat like banging a nail in with a dumptruck.
Even if you can find the resource by looking at the request url you would start splitting the ResortsController into a bunch of ifs and switches even before you got off the ground.
One solution is to add the common action in a module:
module Reporting
extend ActiveSupport::Concern
def show
# the Student or Classroom should be available as #resource
render 'reports/show'
end
included do
before_action :find_resource, only: [:show]
end
private
def find_resource
model = self.try(:resource_class) || guess_resource_class
#resource = model.find(params[:id])
end
# This guesses the name of the resource based on the controller name.
def guess_resource_class
self.class.name[0..-11].singularize.constantize
end
end
class StudentController < ApplicationController
include Reporting
end
# Example where resource name cannot be deduced from controller
class PupilController < ApplicationController
include Reporting
private
def resource_class
Student
end
end
self.class.name[0..-11].singularize.constantize is basically how Rails uses convention over configuration to load a User automatically in your UsersController even without any code.
But the most important key to DRY controllers is to keep your controllers skinny. Most functionality can either be moved into the model layer or delegated out to service objects.
I would put the common logic in the Event Model:
#Event Model
class Event < ...
def self.your_event_method
#self here will be either student.events or classroom.events
#depending on which controller called it
end
end
class StudentsController < ...
...
def show
student = Student.find(params[:id])
student.events.your_event_method
end
end
class ClassroomsController < ...
...
def show
classroom = Classroom(params[:id])
classroom.events.your_event_method
end
end
I have a like model, recording which user liked which record. I used polymorphic association so a user can like many models.
Currently I use nested-resources to handle likes.
POST /items/:item_id/likes
DELETE /items/:item_id/likes/:id
Now for some reasons I want to get rid of the use of like_id by designing a better route. This is because it will be easier to cache a fragment view.
Note that item model is only one of a few models which are likable, and I want to avoid code duplication if possible.
What's a good way to design routes and controllers that will not use like_id but also allows better code reuse in controller?
Possible implementation
I was thinking of routes like this:
POST /items/:item_id/like
DELETE /items/:item_id/like
I won't use nested like resource. Instead I place a like action in items controller. It will determine if the request is a POST or a DELETE and act accordingly. This however doesn't feel DRY.
I don't know about Rails necessarily, but in Zend Framework I would create a front controller plugin to route all requests with methods 'LIKE' and 'UNLIKE' to a particular controller which then deduces which route was requested, and subsequently which resource was requested, and then performs the necessary actions to 'like' or 'unlike' that resource in the name of the requesting user.
Why? Because the user is 'like'-ing or 'unlike'-ing the resource in question, not 'creating a like' or 'deleting a like'. Sure, in the backend, the 'like' is a record in a cache or database that gets created or deleted -- but the semantics of a resource are not necessarily equivalent that of whichever method is used to persist that resource.
What you need is Singular Resources.
routes.rb
resources :items do
resource :like, only: [:create, :destroy]
end
likes_controller.rb
class LikesController < ApplicationController
before_action :load_likeable
def create
#like = Like.where(likeable: #likeable, user: current_user).first_or_create
redirect_back(fallback_location: #likeable)
end
def destroy
#like = Like.find_by(likeable: #likeable, user: current_user).destroy
redirect_back(fallback_location: #likeable)
end
private
def load_likeable
klass = [Recording].detect { |c| params["#{c.name.underscore}_id"] }
#likeable = klass.find(params["#{klass.name.underscore}_id"])
end
end
likes_helper.rb
module LikesHelper
def like_button_for(item)
if item.liked
form_tag recording_like_path(item), method: :delete do
button_tag "UnLike"
end
else
form_tag recording_like_path(item), method: :post do
button_tag "Like"
end
end
end
end
item.liked is method from Item model