Hello Rails Stack community! <3
I want to generate a public sharing URL to a model that should always include some hash to prevent URL guessing.
This is what I came up with:
# routes.rb
resources :reports do
member do
get '/public/:public_hash', to: 'reports#public', as: 'public'
end
end
# In some view
public_report_path(#report, #report.public_hash)
# /reports/1234/public/xxxx-xxxxx-xxxxx-xxxx
This works okay, but I feel like there should be a more graceful way to do this from the routes definition. What I want to do is public_report_path(#report) which should include the public_hash automatically when generating the URL.
Something in the lines of:
# routes.rb
resources :reports do
member do
get :public, do |route_object|
route_object.path.push(route_object.params.first.public_hash)
end
end
end
# In some view
public_report_path(#report)
# /reports/1234/public/xxxx-xxxxx-xxxxx-xxxx
I saw some solutions where the definition of url_for was overwritten I'd rather not overwrite core functionality. Then I prefer giving 2 parameters to the url helper instead.
Save yourself a lot of trouble and use uuids in your rails migrations?
https://guides.rubyonrails.org/v5.0/active_record_postgresql.html#uuid
All you'd need to do is run this in your migration:
create_table : reports, id: :uuid do |t|
t.timestamps
end
Then standard rails routes, relationships etc will be respected.
Related
I have an store application, where I need to make custom routing system where URL stores categories for products. For example, http://example.com/languages/ruby/rails will display category#show named 'rails', that has parent named 'ruby', that has parent named 'languages' and and URL of http://example.com/languages/ruby/rails/store will display product in this category.
Currently I have:
category.rb
belongs_to :parent, class_name: 'Category'
has_many :categories, foreign_key: :parent_id
has_many :products
routes.rb
resources :categories, :path => '', :only => [:index, :show] do
resources :products, :path => '', :only => [:show]
end
root :to => 'products#index'
but it still stacks up to 2, e.g. URL http://example.com and http://example.com/languages shows list of categories/subcategories, but http://example.com/languages/ruby have params: {"action"=>"show", "controller"=>"products", "category_id"=>"language", "id"=>"ruby"}
Removing products from routes does not help at all - then it just says that No route matches [GET] "/language/ruby", although I assume It might cause need for extra check if current URL point on category or product later on.
Also I tried get '*categories/:id', to: 'category#show' variations
+ I am using friendly_id gem so that path do not look like http://example.com/2/54/111/6
I just want to find out what is the best ruby on rails solution for this kind of situations, when you need search engine optimizations + endless (e.g. no way to define how deep such recursion can go) nested resources that nest themselves (including fact that category/language/category/ruby/category/rails just looks ugly).
Note: most information I used is taken from Stack Overflow and railscasts.com (including pro/revised episodes), so mentioning a good source with information like this will be great too.
I solved this myself recently with a CMS I built on Rails recently. I basically construct the routes dynamically at runtime from the database records. I wrote this blog post on the strategy:
http://codeconnoisseur.org/ramblings/creating-dynamic-routes-at-runtime-in-rails-4
The core of the solution (adapting the blog post above) is simply iterate over the database records and construct the routes needed for each category. This is the main class for doing that:
class DynamicRouter
def self.load
Website::Application.routes.draw do
Category.all.each do |cat|
get cat.route,
to: "categories#show",
defaults: { id: cat.id },
as: "#{cat.routeable_name}_#{cat.name}"
end
end
end
def self.reload
Website::Application.routes_reloader.reload!
end
end
For the above, the Category model should implement a "routeable_name" method which simply gives an underscored version of the category name that uniquely names that category's route (its not strictly necessary, but helps when doing "rake routes" to see what you have). and the #route method constructs the full route to the category. Notice the defaults which sets the ID param for the category. This makes the controller action a very simple lookup on the category's ID field like so:
class CategoryController < ApplicationController
def show
#category = Category.find(params[:id])
end
end
i'm doing a course system, and i would like to simplify the routes.
i routed like this:
resources :courses do
resources :modules do
resources :lesson
end
end
and returned this:
/courses/:course_id/modules/:module_id/lesson/:id
/courses/:course_id/modules/:id
/courses/:id
etc...
i want my routes like that:
/courses/:course_name/:module_name/:lesson_name
/courses/:course_name/:module_name/
/courses/:course_name/
etc...
but how?! :(
in the routes file
get "/courses/:course_name/:module_name/:lesson_name", as: :courses
then you should be able to generate the path:
courses_path(course_name: course.name, module_name: module.name, lesson_name: lesson.name)
But I would recommend against it as:
1) This is fighting conventions: don't expect good support for this, none of the new developers who join the project will like you for this.
2) You will have to make sure all course/module/lesson names are unique and url-friendly
3) You'll have to make sure the names never change, because then the urls would change.
I would advice sticking to the default nested paths and overriding #to_param on every module
to smth like:
def to_param
"#{super}-#{name.downcase.gsub(' ', '-')}" # you need a better regex here
end
so the urls look like
/courses/33-computer-science/modules/23-engineering/lesson/56-design-patterns
More about custom routes: http://guides.rubyonrails.org/routing.html#dynamic-segments
In my app I have a User model which defines a history method that returns a list of Activity objects, showing the last N actions the user has carried out. The UserController#history method wires this with a view.
The code looks as follows:
class UserController < ApplicationController
def history
user = User.find(params[:id])
#history = user.history(20)
end
end
class User < ActiveRecord::Base
has_many :activities
def history(limit)
...
end
end
Naturally, I also added this line to my routes.rb file:
match '/user/:id/:action', :controller => 'user'
so now when I go to localhost:3000/user/8/history I see the history of user 8. Everything works fine.
Being a Rails NOOB I was wondering whether there is some canned solution for this situation which can simplify the code. I mean, if /user/8 is the RESTful way for accessing the page of User 8, is it possible to tell Rails that /user/8/history should show the data returned by invoking history() on User 8?
First of all the convention to name controllers is in the plural form unless it is only for a single resource, for example a session.
About the routes I believe you used the resources "helper" in your routes, what you can do is specify that the resource routes to users also has a member action to get the history like this
resources :users do
member do
get :history
end
end
I think there is no cleaner way to do this
You can check it here http://guides.rubyonrails.org/routing.html#adding-more-restful-actions
As far as the rails standards are concerned, it is the correct way to show the history in your case. In rails controllers are suppose to be middle-ware of views and model, so defining an action history seems good to me.
And you can specify the routes in better way as:
resources :user do
get 'history', :on => :member #it will generate users/:id/history as url.
end
I looked around on how to change the dynamic params slot and found this post that does the exact thing.
The post is https://thoughtbot.com/blog/rails-patch-change-the-name-of-the-id-parameter-in
Basically what it does is, if following is the routes:
map.resources :clients, :key => :client_name do |client|
client.resources :sites, :key => :name do |site|
site.resources :articles, :key => :title
end
end
These routes create the following paths:
/clients/:client_name
/clients/:client_name/sites/:name
/clients/:client_name/sites/:site_name/articles/:title
One solution is to override the def to_param method in the model, but I want this without touching the model itself.
But since its for Rails 2.x, how can I achieve the same for Rails 3?
Update
This app is using Mongoid. Not AR.
So, the gem friendly cannot be used afaik.
Rails 4 & 5
In Rails 4, the :param option was added, which seems to do exactly what you're looking for. You can take a look at the Rails 3 code compared to the Rails 4 code.
Details
You can easily implement this in your routes.rb file:
# config/routes.rb
resources :posts, param: :slug
# app/controllers/posts_controller.rb
# ...
#post = Post.find_by(slug: params[:slug])
# ...
As of the release of Rails 4, this functionality is documented in the Rails Guides.
Rails 3
Unfortunately, in Rails 3, the :key option for resources was removed, so you can no longer easily change the name for routes created in this way by just passing in an extra option.
Details
I assume you've already somehow gotten the application working the way you want in the past year, but I will go into a way to get the effect you describe in Rails 3 in routes.rb. It will just involve a bit more work than the to_param method. You can still define custom parameters in routes defined using scope and match (or it's cousins get, put, post, and delete). You simply write in the parameter name you want in the matcher:
get 'clients/:client_name', :to => 'clients#show', :as => client
scope 'clients/:client_name' do
get 'sites/:name', :to => 'sites#show', :as => site
end
You would have to manually add all the routes that resources automatically creates for you, but it would achieve what you're looking for. You could also effectively use the :controller option with scope and additional scope blocks to take out some of the repetition.
EDIT (May 8, 2014): Make it more obvious the answer contains information for both Rails 3 & 4. Update the links to the code to go to exact line numbers and commits so that they should work for a longer period of time.
EDIT (Nov 16, 2014): Rails 4 should be at the top now and include relevant information as it's been the current version of Rails for quite some time now.
EDIT (Aug 9, 2016): Reflect that the solution still works in Rails 5, and update outdated links.
in Rails 4, pass param option to change the :id params. For example
resources :photos, param: :photo_name will generate /photos/:photo_name
In Rails 3 you can rename the id keys by using a combination of namespaces and scopes like this (not very nice though):
namespace :clients do
scope "/:client_name" do
namespace :sites do
scope "/:name" do
post "/:title" => "articles#create"
...
end
end
end
end
If I understand you correctly, what you want is to have the client_name instead of id in your url, right?
You can do that by overriding the to_param method in your model. You can get more information here.
There's a gem for that, just like there's a gem for everything ;)
I've been using FriendlyId for this kind of behaviour in rails 3.
It will require you to add some code to your model though, like this:
class Client < ActiveRecord::Base
has_friendly_id :name
end
...and if your clients don't have URI compatible names, you might want to use a slug for that, which you can do with has_friendly_id :name, :use_slug => true. When using slugs you'll obviously need to persist them to the database as well though.
And as already mentioned, you can still use the to_param trick with rails 3, as documented here. I find FriendlyId a bit more versatile though.
I am using Ruby on Rails 3.2.2 and I would like to generate a proper url_for URL for a nested resource. That is, I have:
# config/routes.rb
resources :articles do
resources :user_associations
end
# app/models/article.rb
class Article < ActiveRecord::Base
...
end
# app/models/articles/user_association.rb
class Articles::UserAssociation < ActiveRecord::Base
...
end
Note: generated named routes are like article_user_associations, article_user_association, edit_article_user_association, ...
When in my view I use:
url_for([#article, #article_association])
Then I get the following error:
NoMethodError
undefined method `article_articles_user_association_path' for #<#<Class:0x000...>
However, if I state routers this way
# config/routes.rb
resources :articles do
resources :user_associations, :as => :articles_user_associations
end
the url_for method works as expected and it generates, for instance, the URL /articles/1/user_associations/1.
Note: in this case, generated named routes are like article_articles_user_associations, article_articles_user_association, edit_article_articles_user_association, ...
However, I think it isn't "good" the way routers are builded / named in the latter / working case. So, is it possible in some way to make the url_for method to work by generating named routes like article_user_association (and not like article_articles_user_association)?
I read the Official Documentation related to the ActionDispatch::Routing::UrlFor method (in particular the "URL generation for named routes" section), but I cannot find out a solution. Maybe there is a way to "say" to Rails to use a specific named router as-like it makes when you want to change the primary key column of a table with the self.primary_key statement...
# app/models/articles/user_association.rb
class Articles::UserAssociation < ActiveRecord::Base
# self.primary_key = 'a_column_name'
self.named_router = 'user_association'
...
end
Your UserAssociation model is in the Articles namespace, which gets included in the named route:
# app/models/articles/user_association.rb
class Articles::UserAssociation < ActiveRecord::Base
...
end
# route => articles_user_association
# nested route => article_articles_user_association
If you remove the namespace, you will get the route helper you are looking for:
# app/models/articles/user_association.rb
class UserAssociation < ActiveRecord::Base
...
end
# route => user_association
# nested route => article_user_association
Unless you have a really good reason for keeping UserAssociation in a namespace, don't.