How do I create a link/route directly to a scope?

2019-09-06 09:20发布

So I have a Post model, that has these scopes:

  scope :unconfirmed, -> { where( status: "unconfirmed") }    
  scope :corroborated, -> { where( status: "corroborated") }
  scope :confirmed, -> { where( status: "confirmed") }

What I want to do is when someone goes to posts/confirmed it shows all the posts scoped to confirmed.

What is the most elegant way to do this? Also, how do I create a path for each scope in my routes.rb?

Edit 1

I know one way to do this is to simply create an action in my posts controller for each enumerable scope (i.e. unconfirmed, etc.).

But I would rather just do all of this in my index action - so I am rendering and using my Index view.

I have tried doing this in my Post#Index:

if params[:unconfirmed]
  @posts = Post.published.unconfirmed
end

But that just gives me a routing error.

This is the error:

Started GET "/unconfirmed" for 127.0.0.1 at 2014-10-27 02:29:51 -0500
Processing by PostsController#show as HTML
  Parameters: {"id"=>"unconfirmed"}
  Post Load (3.0ms)  SELECT  "posts".* FROM "posts"  WHERE "posts"."publication_status" = 1 AND "posts"."slug" = 'unconfirmed'  ORDER BY "posts"."id" ASC LIMIT 1
  Post Load (1.7ms)  SELECT  "posts".* FROM "posts" INNER JOIN "friendly_id_slugs" ON "friendly_id_slugs"."sluggable_id" = "posts"."id" AND "friendly_id_slugs"."sluggable_type" = 'Post' WHERE "posts"."publication_status" = 1 AND ("friendly_id_slugs"."sluggable_type" = 'Post' AND "friendly_id_slugs"."slug" = 'unconfirmed')  ORDER BY "posts"."id" ASC LIMIT 1
Completed 404 Not Found in 28ms

ActiveRecord::RecordNotFound - ActiveRecord::RecordNotFound:
  friendly_id (5.0.4) lib/friendly_id/finder_methods.rb:23:in `find'
   () myapp/controllers/posts_controller.rb:78:in `set_post'

The set_post method is:

  private
    # Use callbacks to share common setup or constraints between actions.
    def set_post
      @post = Post.published.find(params[:id])      
    end

I am using friendly_id for my URLs.

This is my routes.rb:

  get 'posts/:id' => redirect("/%{id}")
  get '/:friendly_id', to: 'posts#show'
  get 'posts/:friendly_id', to: 'posts#show'

Edit 2

I am still getting this error. I believe it to be the case because whenever I try to do /confirmed or any of the other statuses, this is what my log looks like:

Started GET "/confirmed" for 127.0.0.1 at 2014-10-31 18:28:55 -0500
  ActiveRecord::SchemaMigration Load (2.2ms)  SELECT "schema_migrations".* FROM "schema_migrations"
Processing by PostsController#show as HTML
  Parameters: {"friendly_id"=>"confirmed"}
Completed 404 Not Found in 58ms

Which obeys this routing rule:

  get '/:friendly_id', to: 'posts#show'
  get 'posts/:friendly_id', to: 'posts#show'

It still does this even if I put the recommended routing rules for my resources post at the end of my routes file, i.e. like this:

  resources :posts do
    collection do
      get 'confirmed' => 'posts#index', status: 'confirmed'
      get 'unconfirmed' => 'posts#index', status: 'unconfirmed'
      get 'corroborated' => 'posts#index', status: 'corroborated'
    end
  end

  root to: "posts#index"

How do I get the status routes to not be redirected to the posts#index action?

Edit 3

See my full routes.rb

Rails.application.routes.draw do

  %w[privacy terms].each do |page|
    get page, controller: 'info', action: page
  end

  resources :locations      
  devise_for :users, :path_names => { :sign_up => "register", 
                                      :sign_in => "login", 
                                      :sign_out => "logout",
                                                                            :settings => "settings" },
                    :controllers => { :confirmations => "confirmations" }

  devise_scope :user do
    get "login", :to => "devise/sessions#new"
    get "register", :to => "devise/registrations#new"
        get "settings", :to => "devise/registrations#edit"
    delete "logout",   :to => "devise/sessions#destroy"    
  end

  resources :posts do
    collection do
      get 'confirmed' => 'posts#status', status: 'confirmed'
      get 'unconfirmed' => 'posts#status', status: 'unconfirmed'
      get 'corroborated' => 'posts#status', status: 'corroborated'
    end
  end

  get 'posts/:id' => redirect("/%{id}")
  get '/:friendly_id', to: 'posts#show'
  get 'posts/:friendly_id', to: 'posts#show'

  # This supports legacy URLs e.g:
  # http://www.example.com/rbt/26766-algaj-pays-tribute-to-the-honourable-roger-clarke.html   

  get '/rbt/:name', to: redirect {|path_params, _| "/#{path_params[:name].gsub(/^\d+\-/, '')}" } 
  get ':name',      to: 'posts#show'

  root to: "posts#index"

end

Edit 4

The last bit of the puzzle is the original premise of the question, how do I create a link to that newly created path.

The tricky bit is that I want dynamically create the link_path based on the status.

For instance, as a result of the new routes rules, I now have confirmed_path, corroborated_path, unconfirmed_path.

However, I am displaying them in my view like so:

<span class="post-status status label<%=render partial: "shared/color", locals: {post: post.status }%>"><%= post.status.try(:upcase) %></span>

I tried doing this:

<span class="post-status status label<%=render partial: "shared/color", locals: {post: post.status }%>"><%= link_to post.status.try(:upcase), post.status.path %></span>

But that gave me an undefined method path error.

In a normal instance, I would simply do string interpolation and Ruby would render the current string. Given that Ruby is supposed to parse this path, I doubt that would work.

So how do I get that view to automagically generate confirmed_path, corroborated_path, unconfirmed_path dynamically based on what post.status is?

1条回答
家丑人穷心不美
2楼-- · 2019-09-06 09:49

As you see you are lost in routes. Here calling posts/confirmed will go at posts#show instead of your posts#index method instead. The reason is action pack give priority of routes from top to bottom, and since your routes are defined like this:

resources 'posts'
get 'posts/:id' => redirect("/%{id}")
get '/:friendly_id', to: 'posts#show'
get 'posts/:friendly_id', to: 'posts#show'

All posts/unconfirmed, posts/confirmed, etc will never make to the index method as it will be matched with posts#show, and then with get 'posts/:friendly_id', to: 'posts#show'.

Now, even if you move your all get routes to top, it will never make it to other methods you might wanna define later, as all posts/unconfirmed, posts/confirmed, etc will be matched with get 'posts/:id' => redirect("/%{id}") and will be redirected as /confirmed, /unconfirmed, etc.

Recommended way is to have separate methods for individual routes, since in future you also might want to have a different behavior for confirmed posts than corroborated posts. For that your routes will look like this:

resources :posts do
  collection do
    get 'confirmed'
    get 'unconfirmed'
    get 'corroborated'
  end
end

Running $ rake routes|grep posts will give:

            confirmed_posts GET    /posts/confirmed(.:format)                          posts#confirmed
          unconfirmed_posts GET    /posts/unconfirmed(.:format)                        posts#unconfirmed
         corroborated_posts GET    /posts/corroborated(.:format)                       posts#corroborated
                      posts GET    /posts(.:format)                                    posts#index
                            POST   /posts(.:format)                                    posts#create
                   new_post GET    /posts/new(.:format)                                posts#new
                  edit_post GET    /posts/:id/edit(.:format)                           posts#edit
                       post GET    /posts/:id(.:format)                                posts#show
                            PATCH  /posts/:id(.:format)                                posts#update
                            PUT    /posts/:id(.:format)                                posts#update
                            DELETE /posts/:id(.:format)                                posts#destroy

and then you create separate methods, i.e. confirmed, unconfirmed, corroborated etc.

However, you can always point all these routes to one like this(which is not recommended):

resources :posts do
  collection do
    get 'confirmed' => 'posts#status', status: 'confirmed'
    get 'unconfirmed' => 'posts#status', status: 'unconfirmed'
    get 'corroborated' => 'posts#status', status: 'corroborated'
  end
end

Then in your PostsController:

def status
  @posts = Post.published.where(status: params[:status])
end

UPDATE: Change your routes.rb to this -

Rails.application.routes.draw do

  %w[privacy terms].each do |page|
    get page, controller: 'info', action: page
  end

  resources :locations      
  devise_for :users, :path_names => { :sign_up => "register", 
                                      :sign_in => "login", 
                                      :sign_out => "logout",
                                                                            :settings => "settings" },
                    :controllers => { :confirmations => "confirmations" }

  devise_scope :user do
    get "login", :to => "devise/sessions#new"
    get "register", :to => "devise/registrations#new"
        get "settings", :to => "devise/registrations#edit"
    delete "logout",   :to => "devise/sessions#destroy"    
  end

  resources :posts
  get '/confirmed' => 'posts#status', status: 'confirmed', as: :confirmed
  get '/unconfirmed' => 'posts#status', status: 'unconfirmed', as: :unconfirmed
  get '/corroborated' => 'posts#status', status: 'corroborated', as: :corroborated
  get 'posts/:id' => redirect("/%{id}")
  get '/:friendly_id', to: 'posts#show'
  get 'posts/:friendly_id', to: 'posts#show'

  # This supports legacy URLs e.g:
  # http://www.example.com/rbt/26766-algaj-pays-tribute-to-the-honourable-roger-clarke.html   

  get '/rbt/:name', to: redirect {|path_params, _| "/#{path_params[:name].gsub(/^\d+\-/, '')}" } 
  get ':name',      to: 'posts#show'

  root to: "posts#index"

end

and make sure you have status method in your PostsController:

def status
  # your code for fetching posts goes here!
  render text: params[:status] # this is to show an example that `status` is set properly
end
查看更多
登录 后发表回答