Rails: Append number to permalink, if permalink already taken - ruby-on-rails

I would like to give John Doe the permalink john-doe-2, if there already is a john-doe-1.
The number should be the next free one to be appended ("john-doe-n")
Currently my permalinks are generated the usual way:
before_validation :generate_slug
private
def generate_slug
self.permalink = self.name.parameterize
end
How to implement a validates_uniqueness_of-like method, that adds this kind of number to self.permalink and then saves the user normally?

First of all, ask yourself: Is there a simpler way to do this? I believe there is. If you're already willing to add numbers to your slug, how about always adding a number, like the ID?
before_validation :generate_slug
private
def generate_slug
self.permalink = "#{self.id}-#{self.name.parameterize}"
end
This is a very robust way of doing it, and you can even pass the slug directly to the find method, which means that you don't really need to save the slug at all.
Otherwise, you can just check if the name + number already exists, and increment n by 1, then recheck, until you find a free number. Please note that this can take a while if there are a lot of records with the same name. This way is also subject to race conditions, if two slugs are being generated at the same time.

Related

Using alpha-numeric slugs instead of ids in routes - Rails

I am building a Rails 5 team management app which lets users manage organizations and users. I would like to be able to change from using the :id in the path (e.g: /organizations/43) and use an alpha-numeric slug instead (e.g: /organizations/H6Y47Nr7). Similar to how Trello do this (i.e: https://trello.com/b/M9X71pE6/board-name). Is there an easy way of doing this?
I have seen the FriendlyId gem which could take care of the slugging in the path but what would be the best way to generate the slug in the first place?
Ideally, for the most bang for buck the slug would include A-Z, a-z and 0-9 (as I understand it, this is Base58?) and for the sake of not blowing out the url too much, 8 characters at the most. If my calculations are correct, this gives 218 trillion combinations, which should be plenty.
Am I on the right track? Any help would be much appreciated.
Thanks
To create a slug, easiest way is to use SecureRandom. You can add something like the following in your model
before_create :generate_slug
private
def generate_slug
begin
self.slug = SecureRandom.urlsafe_base64(8)
end while Organization.exists?(slug: slug)
end
One small caveat here with respect to what you want is that the slug will sometimes contain an underscore or a dash but that should be fine.
irb(main):014:0> SecureRandom.urlsafe_base64(8)
=> "HlHHV_6rN3k"
irb(main):015:0> SecureRandom.urlsafe_base64(8)
=> "naRqT-NmYDU"
irb(main):016:0> SecureRandom.urlsafe_base64(8)
=> "9h04l4jEEsM"
If you go that route, I would create a table were you save the slugs that you are generating and don't delete them even when you delete an organization. When you create a new organization query this model to make sure there are no duplicates slugs. Also add a unique index in the slug column of the organizations table.
You should not give up the id column with integers so in the show method you will need to do:
org = Organization.where(slug: params[:id]).first

ActiveRecord uniqueness validation prevents update

I'm writing a web app using Rails, part of which includes giving users the ability to leave reviews for things. I wanted to put a validation in the review model to ensure that one user can't leave multiple reviews of the same item, so I wrote this:
class NoDuplicateReviewValidator < ActiveModel::Validator
def validate(record)
dup_reviews = Review.where({user_id: record.user,
work_id: record.work})
unless dup_reviews.length < 1
record.errors[:duplicate] << "No duplicate reviews!"
end
end
end
This validator has the desired behavior, i.e. it guarantees that a user can't review a work twice. However, it has the undesired side-effect that a user can't update an already existing review that he/she left. I'm using a really simple
def update
#review.update(review_params)
respond_with(#work)
end
in the reviews controller. How can I change either the validator or the update method so that duplicate reviews are prevented but updates are allowed?
I'm very new to Rails and web development, so I'm sure I've done something goofy here. I didn't use one of the built-in unique validators because what is unique is the user/work pair; there can more than one review by the same user of different works, and there can be more than one review of the same work by different users.
You can use validates_uniqueness_of on multiple attributes, like this:
validates_uniqueness_of :user_id, :scope => :work_id
Then a user would not be allowed to review a already reviewed work.
#Sharvy Ahmed's answer is definitely the best, as long as the case is simple enough – the OP's case seems like one of them.
However, if the conditions are more complex, you may need/want to write your custom validation. For that purpose, here's an example (checked with Rails 6.0).
class NoDuplicateReviewValidator < ActiveModel::Validator
def validate(record)
dup_reviews = Review.where(user_id: record.user,
work_id: record.work)
dup_reviews = dup_reviews.where.not(id: record.id) unless record.new_record?
if dup_reviews.count > 0
record.errors[:duplicate] << "No duplicate reviews!"
end
end
end
The idea is,
In create, all the relevant DB records retrieved with where can and should be used to judge the uniqueness. In the example new_record? is used to check it out, but it is actually redundant (because nil id matches no records).
In update, the DB row of the record to update must be excluded from the unique comparison. Otherwise, the update would always fail in the validation.
The count method is slightly more efficient in terms of DB transaction.

scaling issues using friendly_id

Okay so I'm working on making a friendly_id that allows me to scale it effectively. This is what I got so far. What it does is essentially add +1 on a count if you have a matching name. So if you are the 2nd person to register as John doe your url would be /john-doe-1.
extend FriendlyId
friendly_id :name_and_maybe_count, use: [:slugged, :history]
def name_and_maybe_count
number = User.where(name: name).count
return name if number == 0
return "#{name}-#{number}"
end
However this piece of code is still bringing me some issues.
If I have created a user called John Doe. Then register another user called John Doe the slug will be /john-doe-UUID. If I register a third user then it will receive the slug john-doe-1.
If I have two users. One that registered with the name first. Say Juan Pablo. Then he changes his name to 'Rodrigo', and then change it back to 'Juan Pablo'. His new slug for his original name will be 'juan-pablo-1-UUID'.
I know this is minor nitpick for most of you but it's something that I need to fix!
You want to overwrite to_param, include the id as the first bit of the friendly id
I extend active record like this in one of my apps,
module ActiveRecord
class Base
def self.uses_slug(attrib = :name)
define_method(:to_param) do
"#{self.id}-#{self.send(attrib).parameterize}"
end
end
end
so I can do
uses_slug :name
in any model
but this alone in any model should work:
def to_param
"#{self.id}-#{self.my_attrib.parameterize}"
end
The benefit of this is that in the controller you when you do
Model.find(params[:id])
you don't have to change anything because when rails tries to convert a string like "11-my-cool-friendly-id" it will just take the first numeric bits and leave the rest.

Is it possible to validate that the title of a new post is not equal to an existing permalink?

In this simplified scenario, I have a model called Post
Each post has a title and a permalink
The permalink is used in the url - i.e. http://mysite.com/blog/permalink
Before a post is created, a callback sets the permalink to be the same as the title
I validate the uniqueness of each title but I do not explicitly validate the uniqueness of the permalink for three reasons:
the permalink is equal to the title, which has already been validated for uniqueness
users have no concept of the permalink, since they are never asked to enter one, so when they get an error telling them that the permalink needs to be unique, they don't have a clue what it means
i'm using mongoid for the ORM and the permalink is the key for each post, and mongoid doesn't seem to let you modify a key
Now imagine the following scenario:
A user creates a post titled "hello" and the url is http://mysite.com/blog/hello
The user changes the title to "goodbye", but the permalink stays the same since it is immutable by design. This is fine, it's what I want and helps to prevents link-rot.
The user then creates a new post, titled "hello" which does not fail the validations since we changed the title of the first post already
The problem here is that we've now got two posts with the same url thanks to our before_create callback.
As I said, I don't want to validate the uniqueness of the permalink as the user in the above scanrio, upon creating the second posts would receive the "permalink must be unique" error and I just know this will confuse them
So I was thinking, is there a validation, or a technique that allows me to prevent a user from creating a post which has a title that is equal to an existing permalink?
If all else fails, then I will just validate the uniqueness of the permalink and write out a really lengthy validation error message that details what the permalink is, and why this one is not deemed to be unique
The stuff we have to think about when making webites, what a carry-on
I can think of four possible solutions:
Validate the uniqueness of permalink like this:
validates_uniqueness_of :permalink, :message => "This title is already taken"
This will present the user with an understandable message.
change your before_create callback to create a numbered permalink
def what_ever_your_callback_is_named
self.permalink = self.title.permalinkify # or however you create the permalink
while self.find_by_permalink(self.permalink)
# check if it is an already numbered permalink
if self.permalink =~ /^(.*)(-(\d+))?$/
self.permalink = "#{$~[1]}-#{$~[3].to_i++}"
else
self.permalink << "-2"
end
end
end
change your before_create to a before_save callback, but than the resulting link is only permanent as long your title doesn't change.
use friendly_id for creating and managing the permalink. It's very powerfull.
Your method that creates the permalink name could check whether that permalink is unique and if not then append a YYYYMMDDHHMMSS value (or some other value to create a unique permalink) to the permalink.
You can write a custom validation method
class Post < ActiveRecord::Base
# You'll need to check only when creating a new post, right?
def validate_on_create
# Shows message if there's already a post with that permalink
if Post.first(:conditions => ["permalink = ?", self.title])
errors.add_to_base "Your error message"
end
end
end

rails: mass-assignment security concern with belongs_to relationships

I've been reading up on rails security concerns and the one that makes me the most concerned is mass assignment. My application is making use of attr_accessible, however I'm not sure if I quite know what the best way to handle the exposed relationships is. Let's assume that we have a basic content creation/ownership website. A user can have create blog posts, and have one category associated with that blog post.
So I have three models:
user
post: belongs to a user and a category
category: belongs to user
I allow mass-assignment on the category_id, so the user could nil it out, change it to one of their categories, or through mass-assignment, I suppose they could change it to someone else's category. That is where I'm kind of unsure about what the best way to proceed would be.
The resources I have investigated (particularly railscast #178 and a resource that was provided from that railscast), both mention that the association should not be mass-assignable, which makes sense. I'm just not sure how else to allow the user to change what the category of the post would be in a railsy way.
Any ideas on how best to solve this? Am I looking at it the wrong way?
UPDATE: Hopefully clarifying my concern a bit more.
Let's say I'm in Post, do I need something like the following:
def create
#post = Post.new(params[:category])
#post.user_id = current_user.id
# CHECK HERE IF REQUESTED CATEGORY_ID IS OWNED BY USER
# continue on as normal here
end
That seems like a lot of work? I would need to check that on every controller in both the update and create action. Keep in mind that there is more than just one belongs_to relationship.
Your user can change it through an edit form of some kind, i presume.
Based on that, Mass Assignment is really for nefarious types who seek to mess with your app through things like curl. I call them curl kiddies.
All that to say, if you use attr_protected - (here you put the fields you Do Not want them to change) or the kid's favourite attr_accessible(the fields that are OK to change).
You'll hear arguments for both, but if you use attr_protected :user_id in your model, and then in your CategoryController#create action you can do something like
def create
#category = Category.new(params[:category])
#category.user_id = current_user.id
respond_to do |format|
....#continue on as normal here
end
OK, so searched around a bit, and finally came up with something workable for me. I like keeping logic out of the controllers where possible, so this solution is a model-based solution:
# Post.rb
validates_each :asset_category_id do |record, attr, value|
self.validates_associated_permission(record, attr, value)
end
# This can obviously be put in a base class/utility class of some sort.
def self.validates_associated_permission(record, attr, value)
return if value.blank?
class_string = attr.to_s.gsub(/_id$/, '')
klass = class_string.camelize.constantize
# Check here that the associated record is the users
# I'm leaving this part as pseudo code as everyone's auth code is
# unique.
if klass.find_by_id(value).can_write(current_user)
record.errors.add attr, 'cannot be found.'
end
end
I also found that rails 3.0 will have a better way to specify this instead of the 3 lines required for the ultra generic validates_each.
http://ryandaigle.com/articles/2009/8/11/what-s-new-in-edge-rails-independent-model-validators

Resources