Determine if action is on member or on collection - ruby-on-rails

I have been trying to find a clean way to determine wether an action (in a rails controller) applies on members or on collection.
For example when you declare in routes.rb
resources :projects
You get the following methods defined "on collections":
index
create
new
and the following are defined on "on members":
update
show
delete
I am trying to find a way to leverage this great pattern in views for example:
<% if #controller.action.applies_on_members? %>
<%= link_to :destroy, :method => "delete", :confirm => "RU Sure" %>
<%= link_to :show, "show" %>
<% end %>
Similarly this could be useful in before_filters
before_filter :find_project, :only_on_members => true
Currently I have to do the following:
before_filter :find_project, :except => [:new, :create, :index, :export_all, :destroy_all, :archive_all]
This is quite annoying and my idea is that all such actions share a common behavior: they are defined on collections.
Does anyone have an idea of how to achieve this in clean way ?
NB: The reason I ask this question is because I am looking for something scalable in terms of development, such that the behavior of before_filters or some partials is automatically inherited by new custom actions.
Who never had to write down something like
<% unless ["new", "index", "some_other_action1", "some_other_action2", "some_other_action3", "some_other_action4"].include? #controller.action_name %>
<%= link_to "Destroy", :project, :method => :delete %>
<% end %>

Almost a year after posting this question, I know have found a reasonable solution for this question.
If we look at what on => :member and on => :collection really do, the difference is basically that on => :member generates a path including the :id param, whereas the on => :collection version does not.
A simple solution would then to test whether params[:id] is blank or not in a custom method. It would look like this (say in application_controller.rb):
class ApplicationController < ActionController::Base
def on_member?
!params[:id].blank?
end
helper_method :on_member? #method will be accessible in views
end
As described in this post for example, we can do something like this in a before_filter:
before_filter :find_project, :if => :on_member?
In views, this allows me to check the kind of method like so:
<% if on_member? %>
<%= link_to :destroy, :method => "delete", :confirm => "RU Sure" %>
<%= link_to :show, "show" %>
<% end %>

First of all, the pattern on operating on collections and their entries is part of REST which is one of the pillars of Rails. What you've identified is essentially what REST calls collections and entries. In theory, you can apply the most common HTTB verbs on either the collection, or an entry within that collection, where: GET; retrieve, PUT; replace, POST; create and DELETE; destroy. Although it's a little unclear what POST would do on a single entry.
In practise, and in Rails, however, you usually use those you've identified:
Collections
Create entry (POST)
View entries (GET)
Entry
Update entry (PUT)
Show entry (GET)
Delete entry (DELETE)
These are the RESTful routes Rails adds. However, as you know Rails also boxes two other actions to implement a user page which can provide the necessary data to these endpoints. (new, edit),
It's rare I see people coming down to these definitions. As I see it there's at least two ways to go about this problem if you really want to solve it (see bottom of the post):
You could monkey-patch "_normalize_callback_options" to get the exact syntax you proposed. This is relatively straight-forward, but if upstream changes anything about the structure, you have to change your patch, thus it's not very sustainable. Therefore I would not recommend that approach, no matter how tasteful the syntastic sugar looks. Of course you could also try to commit this upstream, but it's unlikely to be accepted due to the reasons mentioned below. :-)
Instead I'd put your definitions in an initializer: config/initializer/rest.rb
ENTRY_ACTIONS = [:edit, :new, :show, :delete, :update]
COLLECTION_ACTIONS = [:create, :index]
Then use them in your filters like:
before_filter :find_project, only: ENTRY_ACTIONS
For access in your views, add an entry_action? and collection_action? method to your application helper:
module ApplicationHelper
def entry_action?
ENTRY_ACTIONS.include?(controller.action_name.to_sym)
end
def collection_action?
COLLECTION_ACTIONS.include?(controller.action_name.to_sym)
end
end
Then in your views:
<% if entry_action? %>
# code
<% end %>
I wouldn't extract this to constants myself, but just write the array directly since it's much more clear in my opinion to read when I come back later. Furthermore, most people would have to think twice every time they encounter these methods which is far from optimal.

You're taking a very common problem that's already solved in Rails for us and pretty much turning it upside down. Rather than asking the controller about its state all over your application, you should tell your views and controllers to do what it needs to do however it knows how to do it.
Since you already know for sure that the index, create and new actions apply to "collections" and that update, show and delete apply to "members", you should define each respective controller actions and views to work with such forms of resources.
Going back to the examples you've given, the links should be defined in a partial and have the appropriate view template render the partial.
Your before_filter should be written as before_filter :find_project, :only => [:update, :show, :delete].
Just the thought of seeing calls to #controller.action.applies_on_members? all over the place in your application raises a flag in my mind that you need to rethink your design. Again, you're trying to solve a problem that Rails already takes care of for you.

Related

Ruby on Rails 4: Display Search Results on Search Results Page

I am currently implementing the search functionality in a project and I am struggling displaying it on a dedicated search result page.
Being aware of questions on this topic already but being unable to work out a solution due to utter incompetence, I am asking you for the final pointer :).
The search form spawns on the index page which is entries_path and root_path. I'd like to pass on the parameters to a new page, search_path.
Here are my files:
EntriesController
def search
end
def index
#entries = Entry.all.order('entries.created_at DESC')
#entry = Entry.new # My index page also creates new entries.
if params[:search]
#entries = Entry.search(params[:search]).order("created_at DESC")
else
#entries = Entry.all.order("created_at DESC")
end
Model: entry.rb
def self.search(search)
where("content LIKE ? OR created_at LIKE ?", "%#{search}%", "%#{search}%")
end
routes.rb
Rails.application.routes.draw do
resources :entries
root 'entries#index'
get 'new' => 'entries/new'
get 'show' => 'entries/show'
get 'edit' => 'entries/edit'
get 'search' => 'entries/search'
Finally: the form on index
<%= form_tag(entries_path, :method => "get", class: "search-form") do %>
<%= text_field_tag :search, params[:search], placeholder: "Search for previous entries..", class: "form-control" %>
<% end %>
When I change the entries_path to search_path, I am getting a "We're sorry, but something went wrong. If you are the application owner check the logs for more information." – therefore, I suspect it is a routing problem. However, I can't seem to figure it out. The log says:
ActionController::RoutingError (uninitialized constant Entries):
Phew, would love to know what's going on here! Thanks a bunch already.
Change your routes.rb
Rails.application.routes.draw do
root 'entries#index'
resources :entries do
collection do
get :search
end
end
end
change your path in search form on index page:
<%= form_tag(search_entries_path, :method => :get, class: "search-form") do %>
<%= text_field_tag :search, params[:search], placeholder: "Search for previous entries..", class: "form-control" %>
<% end %>
Change your controller's method:
def search
if params[:search]
#entries = Entry.search(params[:search]).order("created_at DESC")
else
#entries = Entry.all.order("created_at DESC")
end
end
create one template for search method under view/entries/search.html.erb
You can here access your #entries object
Points of changes I have made:
1. Changes in routes.rb:
Rails router recognizes URLs and dispatches them to a controller's action. It can also generate paths and URLs, avoiding the need to hardcode strings in your views.
A resource route maps a number of related requests to actions in a single controller. a resourceful route provides a mapping between HTTP verbs and URLs to controller actions. By convention, each action also maps to particular CRUD operations in a database. for more information regarding routes
You can add additional routes that apply to the collection or individual members of the collection.
For Eg:
To add a member route, just add a member block into the resource block:
resources :entries do
member do
get 'preview'
end
end
To add a route to the collection:
resources :entries do
collection do
get 'search'
end
end
A member route will require an ID, because it acts on a member. A collection route doesn't because it acts on a collection of objects. for more info about
difference between collection route and member route in ruby on rails?
2. Which method should I use for search GET or POST?
There are numbers of post available regarding GET and POST request on the web as well as SO. GET and POST both have their place, and if you’re a Web developer you should understand the pros and cons of each of them. Or if you’re too lazy to do that, just remember that Search forms should use GET method. Your users will appreciate it. ;)
Let me define them in short description.
GET to fetch a resource(when you don't want to make any change in your DB), POST to create a resource(when you want to make a change/create in your DB), PUT (or PATCH, these is debate on the matter) to update a resource, DELETE to delete one.
For your reference:
When do you use POST and when do you use GET?
I hope this information may helps you. Good Luck :)
You could use html as a search field on Index page.
<form>
<legend>Search</legend>
<div class='col-xs-4'>
<input type='text' class='form-control' value='<%= params[:search] %>' name='keyword' placeholder='Keyword' >
</div>
</form>
Note: This form would hit your index action of entries controller, So at this point no need to create a search methods as you have created

Ruby on Rails: Need help updating db attribute in a nested route

Let me preface this by saying, i'm pretty new to rails and programming.
I'm trying to make some links to toggle a boolean attribute on and off. I've essentially succeeded in doing it on a non-nested resource by doing the following:
Route:
resources :my_resource do
get 'toggle_attribute', :on => :member
end
Controller:
def toggle_attribute
#resource = Resource.find(params[:id])
#resource.toggle!(:attribute)
end
View:
<%= link_to "Toggle Resource", toggle_attribute_resource_path(#resource), :remote => true %>
First, like I said above, this works on my non-nested route, however no matter what solution I try to add to the controller I can't get my link to flash a message or re-direct to anything when clicked, you click the button and nothing happens, you have to manually refresh to see the change.
Second, I can't figure out how to get this same sort of thing to work on a route that is nested like so:
Route:
resources :resource_1 do
resources :resource_2
end
Can anyone give me some tips?
Thanks a ton in advance. This stuff has been driving me batty.
By using remote => true, you are telling it to make an ajax call. This means that you need to also add a toggle_attribute.js.erb file in your views folder and in that file use javascript to replace the link element or text with what you want.
Also make sure to respond to js by setting respond_to :html, :js at the top of your controller.
repond_to :html, :js
def toggle_attribute
#resource = Resource.find(params[:id])
#resource.toggle!(:attribute)
end
toggle_attribute.js.erb :
$('#toggler').html("my new html here");
in view:
<%= link_to "Toggle Resource", toggle_attribute_resource_path(#resource), :remote => true, :id => "toggler"%>
Update:
For your nested route try this:
resources :resource_1 do
resources :resource_2 do
member do
get :toggle_attribute
end
end
end
your path would be something like:
toggle_attribute_resource_1_resource_2_path(#resource, #resource2)

Active Admin Custom Action Item Resource Name

Don't know why I can't seem to figure this out since it seems like it should be so simple, but basically, I'm trying to create a link to an action (I want "Publish" to appear next to show, edit, delete) for each of a resource in Active Admin.
I used the code they suggest on their wiki:
action_item do
link_to "button label", action_path(post)
end
Problem is, I get an error because rails doesn't know what "post" is. It's nil. The version of the Wiki on Github has the same code, except they use "resource" instead of post. I wasn't sure if that was them implying that I would use my own resource name there, or if you're supposed to actually use the variable "resource". I tried the latter case and got a "Couldn't find without an ID" error.
So the question is, where do I set the variable name? What are they using as their iterator?
I used to use this:
action_item only: :show do |resource|
link_to('New Post', new_resource_path(resource))
end
UPDATE
action_item only: :show do
link_to('New Post', new_resource_path)
end
Thanks Alter Lagos
I have accomplished this with a very similar piece of code, see:
Inside my: app/admin/posts.rb
member_action :publish, method: 'get' do
post = Post.find(params[:id])
post.publish!
redirect_to admin_post_path(post), notice: 'Post published!'
end
In my case, I want the link buttons available only in the show action.
action_item :only => :show do
if post.status == 'pending'
link_to 'Publish', publish_admin_post_path(post)
elsif post.status == 'published'
link_to 'Expire', expire_admin_post_path(post)
else
end
end
Hope this helps you!
In ActiveAdmin you have to use resource to reference an object that you're working with.
When you use resource in an action like index, you will probably get an error as ActiveAdmin is not working with one. To prevent this, specify the actions you want the button to appear in.
To specify an action, give the argument only with an array of the actions you want the button to appear in. For example:
action_item :only => [:show, :edit] do
...
end

Best practices for static pages in rails app

I am developing a app in ruby on rails for a local business. The pages are 'static', but changeable through a backend CMS I am building for them. Is there a best practice to creating a controller for static pages? Right now I have a sites controller with all static routes, like this.
routes.rb
get "site/home"
get "site/about_us"
get "site/faq"
get "site/discounts"
get "site/services"
get "site/contact_us"
get "site/admin"
get "site/posts"
or would I be better off creating member routes for the site controller like this without the crud, because a 'Site' will not need to have the CRUD.
resources :sites, :except => [:index, :new, :create, :update, :destroy]
member do
get :home
get :about_us
get :faq
get :discounts
get :services
get :contact_us
get :admin
get :posts
end
Or is there a best practice / better way? Any answers would be appreciated. Thanks
If the static pages list are not going to increase, then you can keep the list, but if you want a dynamic list like site/any_new_url , save the routes as
get 'site/:cms_page' => 'cms#show' # all requests matching site/any_page will go CmsController, show method
This will help reduce keep the routes from bloating, but the downside is you do not know what all routes are the valid ones. Your sample code can be
def show
#page_data = Page.find_by_page(:params[:cms_page])
end
show.html.erb
<%= #page_data.html_safe %>
Dunno yet if I consider this a best practice or an abomination but here is what I came up with when tackling the same problem.
My reasoning is that the site was providing some specified functionality (which doesn't really matter for this discussion) + a bunch of information about the organisation itself (about us, contact, FAQ, homepage blurb, whatever). Since all that data was really related to the organisation, an Organisation model seemed reasonable with each of those things as attributes. Here is the model:
class Organisation < ActiveRecord::Base
...validations stuff...
def self.attrs_regex
Regexp.new(self.attrs.join("|"))
end
def self.attrs
self.column_names.reject{|name| name =~ /id|created_at|updated_at/}
end
end
Then I use the attrs class method to generate routes based on the columns. This is in my routes.rb:
Organisation.attrs.each do |attr|
get "#{attr}" => "organisation##{attr}", :as => attr.to_sym
get "#{attr}/edit" => "organisation#edit", :as => "#{attr}_edit".to_sym, :defaults => { :attribute => attr }
post "#{attr}" => "organisation#update", :as => :organisation_update, :defaults => { :attribute => attr}, :constraints => Organisation.attrs_regex
end
The controller gets a little weird and I am not thrilled with the code here but here it is anyway. I need to make sure the attribute is set and available to the views so I can do the right thing there so I set it in the application controller:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :set_attribute
def set_attribute
#attribute = action_name.parameterize
end
end
For the organisation controller I just set the #organisation variable to be the first and only row in the database in the before_filter and then let Rails do its usual magic of calling the method, failing, and rendering a view of the same name. The edit action just uses one view file to edit all the different attributes:
class OrganisationController < ApplicationController
before_filter :set_organisation
def edit
authorize! :edit, #organisation
#attribute = params[:attribute].parameterize
end
def update
authorize! :update, #organisation
#attribute = params[:attribute]
respond_to do |format|
if #organisation.update_attributes(params[:organisation])
format.html do
redirect_to "/#{#attribute}", notice: t('successful_update')
end
format.json { head :ok }
else
format.html { render action: "edit" }
end
end
end
private
def set_organisation
#organisation = Organisation.first
end
end
So that is where I ended up. Like you I hit up SO to tap into the seething mass of genius here but ended up with disappointing results. If there is something better out there I am still hoping to find it.
What I like about what I did is that routes are automatically generated based on the structure of the organisation table.
What I don't like about what I did is that routes automatically generated based on the structure of the organisation table.
I know I will pay for that design decision when I have to deal with i18n routing and there are probably a thousand other reasons that this is a bad idea that I have yet to discover but for the moment I have a happy client.
In the end this is not a suggestion that you should do this, but I am hoping to give you more than I got so you can advance your thinking on this and hopefully end up a little closer to that best practice.
If you are going to construct a CMS, which likely connects to a database, and allow your customer to change the text on the pages of their site, I would not recommend using static pages. In Rails terms, a static page would refer to creating html files in your /views/pages directory. If you go this route, then you're walking outside of the way that Rails was designed.
I believe that what you want to do is create tables in the database that correspond to and store the data for your posts, etc. You can pull information into the controller from the model that it corresponds to and then user a view to display the data. You can create a layout for these pages and then create controllers for each of the pages that you add.
As far as routes, I would recommend using the following:
map.resource :controller_name
you then would add the code that displays the information from the CMS in the corresponding show controller action and view for each page.

How to define a controller function that works for instances of any model

Implementing versioning for a Rails app I'd like to have a view that displays all versions of a model with some extra functionality like reverting etc.
I use the paper_trail gem for the versioning.
I know that I could do that by writing a controller function like versions and a view for every model but I'd like to do it for all models at once. This should be possible because the model.versions attribute is always structured identically.
Ideally the URL should look like /pages/testpage/versions while testpage is the page id.
This seems similar to the concept of nested routes in rails.
resources :pages do
resources :versions
end
The problems with nested routes however are:
Needs extra configuration per model
I cannot access the testpage object without knowing of which model it is an instance.
I also wasn't able to find a way to determine the model since the only thing that is provided to my versions controller is the params hash.
I'm completely open to alternative solutions that might not follow my initial ideas.
Write it in your ApplicationController and define it as a helper_method.
For example
class ApplicationController < ActionController::Base
helper_method :current_time
def current_time
Time.now
end
end
Now you can cal current_time everywhere in controllers or views.
Also you can write separate Module/Class and define there your helpers methods. Than you should include this file into your ApplicationController as well
UPD after theme is changed
I didn't think about your actual question. But I can say that your approach is nod the best here.
You should create new resource instead of creating new functionality which will hard to be tested. So create new resource (controller): versions and play around this controller.
For example how it can work:
/versions/pages/132
/versions/comments/1003
How to realize it:
match "/versions/:model/:id", :to => "versions#index"
In your controller:
class VersionsController < ActionController::Base
def index
#object = my_type.find(params[:id])
#versions = #object.versions
end
private
def my_type
params[:model].constantize
end
end
Of course you can change routes the way you want:
match "/:model/:id/versions", :to => "versions#show"
So now your pretty /pages/testpage/versions will work fine for you without any new strange logic.
UPD 2
Imagine you have got this route:
match "/:model/:id/versions", :to => "versions#index", :as => :versions
And this objects:
#page = Page.last
#hotel = Hotel.find(123)
#comment = #page.comments.first
How will we create links for versions:
<%= link_to "Versions of this page", versions_path(:model => #page.class.to_s, :id => #page.id) %>
<%= link_to "Versions of this hotel", versions_path(:model => #hotel.class.to_s, :id => #hotel.id) %>
<%= link_to "Versions of this comment", versions_path(:model => #comment.class.to_s, :id => #comment.id) %>
I would suggest passing a param such as 'type' and stuff the model name there. Then in your controller you can do:
class VersionsController < ApplicationController
def index
model = params[:type].classify.constantize
#obj = model.find(params[:id])
end
end
For your links, you can pass queries to the link_to helper
<%= link_to versions_path(#model, :type => #model.class) %>
Or something along those lines.

Resources