Best practices to implement "complex" urls in Rails - ruby-on-rails-3

I am developing an enterprise application and would like to use urls with long / non trivial ids instead of the regular, incremental ids.
For instance I'd like to have something like http://mysite.com/users/23lkfjls0934 or http://mysite.com/companies/23lkfjls0934 instead of http://mysite.com/users/3 or http://mysite.com/companies/company-name
What are the steps to follow (migrations, associations, models...) to achieve this result and what is important to keep in mind while building this feature?

You could use friendly_id gem to do the contrary: make URLs as not-friendly as possible :)
Add a new field to your table, it will store your long key. Then use it like this:
class User < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: :long_random_key
end
You can initialize this long key in a before_create hook, for example.
class User < ActiveRecord::Base
extend FriendlyId
friendly_id :name, use: :long_random_key
before_create :generate_long_random_key
private
def generate_long_random_key
self.long_random_key = SecureRandom.hex(32)
# TODO: Make sure it's unique
end
end

Related

How does Devise hide some fields when output json

I am using Grape + Mongoid + Devise.
I found that the Devise user model have more fields (e.g. encrypted_password, sign_in_count, last_sign_in_at) than the user json output when I wrote API response.
I have searched in Devise code, didn't find anything like custom to_json, how does Devise achieve that?
I'm not sure on Grape, but on Rails you can do it with a serializer (as Grape has many code compatible with Rails, I think there's a big chance to work).
To use a serializer, you need to include the "active_model_serializers" gem.
Example:
class UserSerializer < ActiveModel::Serializer
attributes :id, :email, :username
end
On this example, Devise will always print only these 3 fields on a JSON output.
To include all attributes except some of them, you can do something like this:
class UserSerializer < ActiveModel::Serializer
attributes(*(User.attribute_names - ["date_created", "first_name"] ).map(&:to_sym))
end
Also, at least on Rails, you'll want to remove the root from the output. To do this, add this code to your application_controller.rb:
def default_serializer_options
{root: false}
end

Storing user_id in the paper_trail versions table

I am using paper trail to audit changes to data and would like to store the user_id of the current user in addition to the "whodunnit" column that paper_trail stores by default.
I had no trouble modifying the versions migration to add the user_id column. But I haven't figured out an easy way to set that column from the various models in my app.
It seems like this should work:
has_paper_trail :meta => { :user_id => current_user.id
}
And, I think it might work if I had access to the current_user in my models. But I don't. After researching how to get access to the current_user in my model, I see there is a philosophical debate here. That's not my question though.
So I'm thinking of using a gem like sentient_user or sentient_model to give me access to the current_user in my models so I can set it with something like the code above.
However, adding these gems seems complicated for the little thing I'm trying to do here. I'm wondering if there is an easier way.
What is the easiest way to add the user_id of the person who took the action to the versions table?
The current_user don't exists in models by itself, it appears from controller. So, standard approach is applicable:
class ApplicationController < ActionController::Base
def user_for_paper_trail
current_user if user_signed_in?
end
def info_for_paper_trail
{ user_id: current_user.id } if user_signed_in?
end
end
# config/initializers/paper_trail.rb
class Version < ActiveRecord::Base
attr_accessible :user_id
end

Retrieving sublist 3 level deep in Rails

I have a datamodel that contains a Project, which contains a list of Suggestions, and each Suggestion is created by a User. Is there a way that I can create a list of all distinct Users that made Suggestions within a Project?
I'm using Mongoid 3. I was thinking something like this, but it doesn't work:
#project = Project.find(params[:id])
#users = Array.new
#users.push(#project.suggestions.user) <-- this doesn't work
Any ideas? Here's my model structure:
class Project
include Mongoid::Document
has_many :suggestions, :dependent => :destroy
...
end
class Suggestion
include Mongoid::Document
belongs_to :author, class_name: "User", :inverse_of => :suggestions
belongs_to :project
...
end
class User
include Mongoid::Document
has_many :suggestions, :inverse_of => :author
...
end
While Mongoid can give MongoDB the semblance of relationships, and MongoDB can hold foreign key fields, there's no underlying support for these relationships. Here are a few options that might help you get the solution you were looking for:
Option 1: Denormalize the data relevant to your patterns of access
In other words, duplicate some of the data to help you make your frequent types of queries efficient. You could do this in one of a few ways.
One way would be to add a new array field to User perhaps called suggested_project_ids. You could alternatively add a new array field to Project called suggesting_user_ids. In either case, you would have to make sure you update this array of ObjectIds whenever a Suggestion is made. MongoDB makes this easier with $addToSet. Querying from Mongoid then looks something like this:
User.where(suggested_project_ids: some_project_id)
Option 2: Denormalize the data (similar to Option 1), but let Mongoid manage the relationships
class Project
has_and_belongs_to_many :suggesting_users, class_name: "User", inverse_of: :suggested_projects
end
class User
has_and_belongs_to_many :suggested_projects, class_name: "Project", inverse_of: :suggesting_users
end
From here, you would still need to manage the addition of suggesting users to the projects when new suggestions are made, but you can do so with the objects themselves. Mongoid will handle the set logic under the hood. Afterwards, finding the unique set of users making suggestions on projects looks like this:
some_project.suggesting_users
Option 3: Perform two queries to get your result
Depending on the number of users that make suggestions on each project, you might be able to get away without performing any denormalization, but instead just make two queries.
First, get the list of user ids that made suggestions on a project.
author_ids = some_project.suggestions.map(&:author_id)
users = User.find(author_ids)
In your Project class add this :
has_many :users, :through => :suggestions
You'll then be able to do :
#users.push(#project.users)
More info on :through here :
http://api.rubyonrails.org/classes/ActiveRecord/Associations/ClassMethods.html#method-i-has_many
For mongoid, take a look at this answer :
How to implement has_many :through relationships with Mongoid and mongodb?

How to user defined friendly URLs in Rails 3?

Now i have something like this
http://myapp.com/pages/1
http://myapp.com/pages/2
http://myapp.com/pages/3
http://myapp.com/pages/4
And each page belong to one user
What i need is to each user to set it's own custom name for the page.
I was thinking of using the friendly_id gem http://norman.github.com/friendly_id/
but I don't find any method to directly edit the slug to set a custom friendly url
how should i proceed?
FriendlyID is a great gem.
It shouldn't be hard to implement user defined page URL.
Create table pages with user_id and link
class User < ActiveRecord::Base
has_many :pages
class Page < ActiveRecord::Base
belongs_to :user
has_friendly_id :link # link is name of the column whose value will be replaced by slugged value
On the page#new you add an input for the link attribute.
Alternatively, you could set friendly_id on title or something else with :use_slug => true option. This way FriendlyID will take the title and modify it so it doesn't have and restricted characters. It will use it's own table to store slugs. Use cached_slug to increase performanse.
Updated
To give users a choice whether they wan't to set a custom link, you could do this:
Set friendly_id on the link field without slugs..
Make a virtual attribute permalink so you could show it in your forms.
In the before_filter, check whether the permalink is set.
If it is, write it to the link field.
If it's not, write title to the link field.
FriendlyID uses babosa gem to generate slugs. If you decide to use it as well, this is how your filter could look like:
protected
def generate_link
#you might need to use .nil? instead
self.link = self.permalink.empty? ? make_slug(self.title) : make_slug(self.permalink)
end
def make_slug(value)
value.to_slug.normalize.to_s #you could as well use ph6py's way
end
Adding to_param method to one of the models should help:
def to_param
"#{id}-#{call_to_method_that_returns_custom_name.parameterize}"
end
Hope this is what you are looking for :)
I am not using the friendly_url gem and am not sure whether my way is efficient. But it works fine for me.
I have a model called Node with id and friendly url field called url_title.
My routes.rb file:
resource 'nodes/:url_title', :to => 'Nodes#view'
nodes_controller.rb
class NodesController <ActiveController
def view
#node = Node.find_by_url_title(:params(url_title))
end
end
And use the #node variable to populate your view.
Now, whenever I type www.example.com/nodes/awesome-title , it takes me to the proper page. One argument against this can be need to create an index on a non-primary field. But I think that might be required for better performance even in the friendly_url gem. Also, the non-primary field url_title needs to be unique. Again, this might be required even for correct working for friendly_url .
Feel free to correct me if I am wrong in these assumptions.
There are a variety of ways, you can achieve this-
1) using Stringex
2) sluggable-finder
3) friendly_id
A complete step by step methodology with reasons for each to be used can be found out here. Happy reading!

rails and namespaced models issue

Using rails 3/3.1 I want to store invoices with their items (and later more associations like payments, etc…).
So in a first approach I set up the models like this:
class Invoice < ActiveRecord::Base
has_many :invoice_items
end
class InvoiceItem < ActiveRecord::Base
belongs_to :invoice
end
And the routes likes this:
resources :invoices do
resources :invoice_items
end
I chose InvoiceItem instead of Item because I already have a model named Item and I somehow want to namespace the model to invoices. But this name has the huge disadvantage that one has to use invoice.invoice_items instead of a intuitive invoice.items. Also the generated url helpers look real ugly, for example "new_invoice_invoice_item_path(invoice)" (notice the double invoice_invoice).
So I changed to namespaced models like this:
class Invoice < ActiveRecord::Base
has_many :items, :class_name => "Invoice::Item"
end
class Invoice::Item < ActiveRecord::Base
belongs_to :invoice
end
And the routes likes this:
resources :invoices do
resources :items, :module => "invoice"
end
Now the assocation is named nicely and also the url helpers look pretty. But I can't use dynamic urls (ex. [:new, invoice, :item]) anymore, because the controller is set to "invoice_item" instead of "invoice/item".
I wonder how other people solve this problem and what I'm doing wrong. Or is this simply a bug in rails 3.0.7/ 3.1.rc?
EDIT:
Sorry, I seems I didn't correctly express my concern. My model Item is not related to Invoice::Item. Order::Item is also not related to Item nor Invoice::Item. An Invoice::Item can only belong to one invoice. An Order::Item can only belong to an Order. I need to namespace - but why doesn't rails properly support namespacing out of the box? Or what am I doing wrong with namespacing?
Corin
If an order item and an invoice item are not the same object in the real world then I would name them differently rather than trying to namespace, for example OrderItem and InvoiceItem - this will keep things clearer as your codebase grows and avoid the need to make sure you use the right namespace everywhere you reference an Item.