I am trying to create a unique numeric permalink in rails. My problem is that I need to make sure it is unique, has between 5 and 7 numbers and is randomly generated (so not simply counting up). I did look at FriendlyID but I am not sure if this can deliver what I need - the url for my permalink should eventually look like this:
www.kreelu.com/4325677
Is there a build in feature or a gem that can provide this?
Thanks!
after_validation :set_permalink
def rand_permalink #you can find a better way to exclude loop db-searches
r = rand.to_s[2..8] # 7-digit random, you can make [2..11] for 10-digits and so on
while find_by_permalink(r).present?
r = rand.to_s[2..8]
end
r
end
def set_permalink
permalink = rand_permalink unless permalink.presence
end
Assuming you want to create the unique permalink on create, you want to store it in your database and the class is named Post:
validate :permalink, :uniqueness => true
before_create :create_permalink
private
def create_permalink
loop do
self.permalink = Array(1..7).map{ rand(10).to_s }.join
return if Posts.where(permalink: permalink).blank?
end
end
Related
I am very new to friendly_id and it matches my need to provide friendly URLs 👍
I have a Group model (i.e. a group of users) for which I generate a unique code upon creation. FYI, this code attribute is set as a unique index in my PostgreSQL database.
class Group < ApplicationRecord
include FriendlyId
friendly_id :code
after_create :generate_group_code
private
def normalize_friendly_id(value)
super.upcase
end
def generate_group_code(size = 8)
allowed_chars = ("a".."z").to_a
code = (1..size).map { allowed_chars[rand(26)] }.join while Group.exists?(code: code)
update(code: code)
end
end
I think I have followed the gem's guide properly, I just want the generated code to be upcased in the URLs (i.e. /group/ABCDEFGH).
The friendly_id is indeed set as my code attribute, but it is not upcased. I placed a byebug in the normalize_friendly_id method but it is never triggered. What am I missing?
The normalize_friendly_id is only called when using the slugged module to use a slug column to store and find by this column:
friendly_id :code, use: :slugged
Using this you can then override the normalize_friendly_id method.
Sunny's way is probably the way to go in general, as the slugged module is required to edit internal methods such as normalize_friendly_id.
In my case, I already have a code attribute that is unique. Using the slugged module would create a new attribute called slug, which would be exactly the same as my code. I want to avoid that duplication.
In the end, I decided to dodge the friendly_id gem and directly override the to_param method the my model (inspired by this gist):
class Group < ApplicationRecord
validates :code, format: { with: /\A[a-z]{8}\z/ }, uniqueness: true, on: :update
after_create :generate_group_code
# Override the method to allow '/group/MYGROUPA' paths
def to_param
code.upcase
end
# A group code is made of letters exclusively.
# Converting a String (or nil) to an integer leads to 0.
# In this case, lookup by code, otherwise, lookup by id.
# Note the 'downcase', which allows URLs to be case insensitive.
def self.find(input)
input.to_i == 0 ? find_by_code!(input.downcase) : super
end
private
def generate_group_code(size = 8)
allowed_chars = ("a".."z").to_a
code = (1..size).map { allowed_chars[rand(26)] }.join while Group.exists?(code: code)
update(code: code)
end
end
I'll edit this answer if I encounter any side effect, but for now it works.
I want to auto generate a hash value from within the model.
The user creates a resume, they then have the option to share it by clicking a share button which auto generates a unique (random hashed string) url tied to that specific resumes view.
class ResumesController < ApplicationController
def share
#resume = Resume.find(params[:id])
#share = Share.new
#share.resume_id = #resume.id
#share.save
redirect_to action: 'index'
end
end
My Share model has two columns, resume_id, which I already set in the controller, andhash_url` which I want to automatically set in the model.
class Share < ActiveRecord::Base
attr_accessible :label, :resume_id, :url
end
My question is, how do I go about creating a unique hash and store it in the hash_url column? Also, I'm assuming before it saves it will have to check the share table to make sure it is not saving a hash that already exists.
You can generate and store a hash before saving the object. Add something like this to your model:
# share.rb
before_validation :generate_hash
private
def generate_hash
self.hash_url = Resume.find(resume_id).content.hash
end
The hash method is a method Ruby provides: http://ruby-doc.org/core-2.1.1/String.html#method-i-hash It returns a hash based on the string's length and content.
before_create
I'm guessing you want to send users to the likes of:
domain.com/resumes/your_secret_hash_url #-> kind of like a uuid?
The way I would do this is to use the before_create callback with SecureRandom. Whilst this won't give you a unique value, you can check it against the form:
#app/models/resume.rb
Class Resume < ActiveRecord::Base
before_create :set_hash
private
def set_hash
self.hash_url = loop do
random_token = SecureRandom.urlsafe_base64(nil, false)
break random_token unless Resume.exists?(token: random_token)
end
end
end
Reference: Best way to create unique token in Rails?
This will give you the ability to set the hash_url on create, and have it be unique
I am trying to rename the routing of my email inputs for my rails app. Currently, when someone inputs their email, it routes them to /:controller/:id
I want to do it such that once they input their email, it won't show their id so they cannot change the id number and see other people's emails.
I think, use a permalink with some alphanumeric give secure email from another. for Assumes you have people model.
You only need to add one attribute to people, attribute name is permalink
try run
rails g migration add_permalink_to_peoples permalink:string
rake db:migrate
On people.rb , play with before_save
class People < ActiveRecord::Base
attr_accessible :email, :permalink
before_save :make_it_permalink
def make_it_permalink
# this can create permalink with random 8 digit alphanumeric
self.permalink = SecureRandom.base64(8)
end
end
And your routes.rb
match "/people/:permalink" => 'peoples#show', :as => "show_people"
on peoples_controller.rb
def show
#people = People.find_by_permalink(params[:permalink])
end
Run rails server and add one people.
People have a unique permalink with random 8 digit alphanumeric (e.g /people/:permalink)
example : http://localhost:3000/people/9sd98asj
EDIT:
i was trying my self and this dosn't works if its an email, but if you try it with a username with no special carracters it will work fine !
try something like this:
routes:
match "/controller/:username" => "controller#show"
controller:
def show
#user = find_by_username(params[:username])
end
I wanted some advice about how to handle to_param in regards to permalinks
Basically this is what happens.
Create a new company
The company :name is then parameterized and saved as a :permalink in the db
Updating an existing company enables you to change the :permalink
There are validations to ensure user updated :permalink is unique
The problem I'm having is occurring when updating the company's :permalink to something that already exists. The uniqueness validation works which is great, but it changes the params[:id] to the invalid permalink instead of reseting and using the existing params[:id]
When I try to edit the permalink to something else I get a flash validation error of "Name already taken" because it thinks I'm editing the company of the already existing :permalink (company). The URL reflects the change in permalink since my companies_controller.rb is using #company = Company.find_by_permalink[:id])
I wanted to know the best way to handle this issue?
class Companies < ActiveRecord::Base
before_create :set_permalink
before_update :update_permalink
attr_accessible :name, :permalink
validates :name, :permalink, uniqueness: { message: 'already taken' }
def to_param
permalink
end
private
def set_permalink_url
self.permalink = name.parameterize
end
def update_permalink_url
self.permalink = permalink.parameterize
end
end
Apologies if I'm not making too much sense.
Thanks in advance.
you could try to handle this with an after_rollback callback.
after_rollback :restore_permalink
def restore_permalink
self.permalink = permalink_was if permalink_changed?
end
here's how it works : every update / destroy in Rails is wrapped in a transaction. If the save fails, the transaction rollbacks and triggers the callback.
The callback then restores the old value (permalink_was) if it was changed since the record has been loaded.
See ActiveModel::Dirty and ActiveRecord::Transactions for more info.
EDIT
On the other hand, there may be another solution (untested) - just define your accessor like this :
def permalink=( value )
permalink_will_change! unless #permalink == value
#permalink = value
end
This way, the permalink will not be marked as dirty if the new value is identical to the old one, and so AR will not try to update the column.
Explanation:
i don't know on which version of rails it was implemented (it is relatively recent), but here's how "dirtyness" works :
your "standard" (automagically generated) attribute setters basicly call
#{your_attribute}_will_change! before setting the associated
instance variable (even if you set the exact same value than before)
when you call save, ActiveRecords looks for attributes that have changed ("dirty") and builds the SQL UPDATE query using ONLY these attributes (for performance reasons, mostly)
so if you want to avoid your permalink to appear in the query when it is unchanged, i think you have to override the standard setter - or avoid mass-assignment and only set permalink if it has changed
First Item
I Want to validate a field to make sure it is unique (in the last 6 months) before saving it to the database.
I am thinking I should use validates_uniqueness_of :field, case_sensitive => false, Scope => ...
For my application it only has to be unique if, it was used <6 months ago.
Thinking to compare it to created_at, but don't really know how to go about it.
Second Item
I think I should somehow use .strip to remove any spaces before or after the text that the use may have put in accidentally (I know that these extra spaces are used by default in rails and if they are there can make a filed unique.)
If anyone has any hints on how this should be done correctly I really would appreciate it.
validates_uniqueness_of works by checking if a record already exists with the same value of the given field within the given scope. :scope lets you define the scope (obviously) of the uniqueness; for instance, if I was creating blog software and wanted to only allow a post title to be used once per blog, I could say validates_uniqueness_of :title, :scope => :blog_id -- without the scope, I'd only be allowing each title to be used once across the entire system. :scope won't let you do a complex check, like that which you desire.
What you're probably need to do is create your own validation function to check the uniqueness of the field in question within the given timeframe (code goes within the model):
validate :field_must_be_unique_within_six_months
def field_must_be_unique_within_six_months
return if field.blank?
num_duplicates = self.class.count(:conditions => ["field = ? AND created_at < ?", self.field, 6.months.ago])
if num_duplicates > 0
errors.add(:field, :taken)
end
end
The field_must_be_unique_within_six_months method will work similarly to validates_uniqueness_of, in that it will add an error message if there is already a record with the same given field, but with the added condition that it will also check the date. The validate :field_must_be_unique_within_six_months will add the method to the validation process when a record is saved.
To validate multiple fields at the same time without violating DRY, you could use validates_each to do something like the following:
validates_each :field1, :field2 do |record, attr, value|
if record.class.exists?(["#{attr.to_s} = ? AND created_at < ?", value, 6.months.ago])
errors.add(attr, :taken)
end
end
In the above block, record is the record being validated, attr is the attribute (so field1, field2, etc.) and value is the value of that attribute.
You can probably do something like this:
def validate
errors.add(:field, 'blah blah') if is_used_recently && !has_unique_field?
end
def has_unique_field?
Model.exists?(['field = ? and created_at > ?', self.field, 6.months.ago])
end
def is_used_recently
self.created_at < 6.months.ago || self.new? # i don't know if created_at would be set by this point
end
Alternatively you might want to create a new validation handler, or extend the existing one to pass in a :within option if that's something you're going to be doing often.
To get rid of leading and trailing white space the method you want is 'strip'. You can run this on all your fields by doing something like:
before_validation :clean_up_whitespace
def clean_up_whitespace
self.some_field.strip! # this does the strip in place
end
I hope this helps, let me know if I've made any mistakes!