How can I create complex unique using Rails? - ruby-on-rails

I have to make a field that contains a http link to be unique in a Rails model. In addition to that, I would like to reject link variations as well like:
If I try to insert an httpS address when an http address exist
If I try to insert a link with parameters when there is another link without parameters or different ones
I have found this link that may indicate the answer of my problem but I hope that I can do this directly in the index as my case doesn't change as time passes.
In Rails, How do I validates_uniqueness_of :field with a scope of last 6 months
There is no need to provide the exact answer as I have some experience with regular expression. A different example or a reference already would be wonderful as I couln't find none

Something like this can give you the first steps:
require 'uri'
class Link < ActiveRecord::Base
attr_accessor :link
validate :check_link_integrity
validates :url, uniqueness: true
before_save :parse_link
def check_link_integrity
begin
URI::parse(self[:link])
return true
rescue URI::InvalidURIError => e
return false
end
end
def parse_link
uri_link = URI::parse( self.attributes.delete(:link) )
self.url = uri_link.host + uri_link.path
self.protocol = uri_link.scheme
self.parameters = uri_link.query
end
def to_s
"#{protocol}://#{url}?#{parameters}"
end
end
URI module documentation: http://www.ruby-doc.org/stdlib-1.9.3/libdoc/uri/rdoc/URI.html

You could do that with a custom validation method using an URI module
All you need is
uri = URI(your_url)
and then basically check if there is another record with the same uri.host and uri.path.
Yes, this is not a rails way and this will require calculations on each validation, however your request is not simple as well.
Another way would be to store only hostname + path in the database (meaning: to cut protocol and parameters from url during saving). This way you can use built-in uniqness check mechanism

Related

Update attribute in associated model in Rails when parent is updated

I have two models User and Assignment. Whenever User is updated I want to update the url attribute in Assignment.
How do I do this?
class User
has_many :assignments
...
end
class Assignment
belongs_to :user
before_save :set_url
def set_sandbox_url
language = 'www'
project = 'example'
base_url = "https://#{language}.#{project}.org/"
sandbox_url = "#{base_url}/User:#{user.username}/#{article_title}"
end
I agree with Tamer Shlash that there is not really a benefit in storing this URL in the database because it can be easily generated each time you need it.
But apart from that, I would like to answer your question. Your callback to regenerate the URL doesn't work for various reasons. First, you want to update the URL when the user changes therefore the user needs to have a callback defined. Second, the naming is not correct. The callback as it is currently written would try to run a set_url method but the method is actually called set_sandbox_url. And third, sandbox_url = will just assign the new URL to a local variable sandbox_url but it would not update the instance variable #sandbox_url.
I would do something like this:
# in app/models/user.rb
after_save :update_assignment_url
private
def update_assignment_url
assignments.each(&:update_url) if username_previously_changed?
end
# in app/models/assignments.rb
def update_url
language = 'www'
project = 'example'
base_url = "https://#{language}.#{project}.org/"
sandbox_url = "#{base_url}/User:#{user.username}/#{article_title}"
update!(sandbox_url: sandbox_url)
end
Note: because you build the URL by simply concatenating strings I suggest making sure that these strings (especially values provided by the user like username and article_title) only include characters that are valid in an URL (for example by using String#parameterize).
You might want to read about Dirty Attributes too which provided the used username_previously_changed? method.

Modifying value using before_validation before validate_uniqueness_of - SystemStackError: stack level too deep

I'm trying to ensure the uniqueness of some urls we store in my app.
I want to strip them of their query parameters as these can change for the same url, and certain urls needs a slightly different stripping algorithm.
I have a method (in an initializer)
def clean_url(url)
domain = get_domain(url)
case domain
when "xyz.com" || "xyz.co.uk"
url = url.split("&")[0]
when "abc.com"
url = url.split("?")[0]
end
end
I want to call url_cleaner before_validation
thing.rb
before_validation :url_cleaner
validates_uniqueness_of :url
def url_cleaner
self.url = clean_url(self.url)
end
And I'm getting
SystemStackError: stack level too deep
What's happening here is that your before_validation method is trying to save the object, which in turn kicks off the before_validation and it goes into a loop - that's what's causing SystemStackError: stack level too deep
What's causing some confusion here is that your choice of local variable (url) within that method is the same as the actual attribute "url", which is accessed via the method "def url". You see what i mean? lots of different things called "url" here. I'd make it so that you always just refer to the one thing, which is self.url. I'd rewrite the above like so:
before_validation :clean_url
validates_uniqueness_of :url
def clean_url
case get_domain(self.url)
when "xyz.com" || "xyz.co.uk"
self.url = self.url.split("&")[0]
when "abc.com"
self.url = self.url.split("?")[0]
end
end
Does it still go into a loop if you do this? If so, can you add the definition of the get_domain method to your question? Maybe something in here is trying to save or validate the object.
As per Max's logic above, I ended up doing the following:
before_validation(on: :create) do
self.ad_url = clean_url(self.url) if attribute_present?("url")
end
And that did the trick

Ruby on Rails: proper method of validating Model data comparing with parameter(s)

Using rails version 4.0.5 currently. I can't seem to find a canonical answer to whether this is the best (DRY, MVC, etc.) method to validate what seems to me to be a simple validation. In my model, I have a unique document_id. This document_id should appear in the filename (in this case, always xml files) e.g. blah/blah.document_id.xml and it should also appear as an attribute on the root element of the xml document e.g. id='document_id'. I'd like to write (assuming the document_id and filename don't match):
doc = Document.new
...
if !doc.valid?
puts doc.errors[:document_id] # <= 'document_id does not match filename'
end
Here's the closest I've gotten:
doc = Document.new
...
if !doc.valid?
... # no error caught here
end
if doc.document_id_matches_filename(xml_file)
puts 'document_id does not match filename'
end
Inside app/models/document.rb, I have this:
class Document < ActiveRecord::Base
...
def document_id_matches_filename(filename)
return self.document_id == File.basename(filename, '.xml')
end
end
I could add a filename column to the Document model and use doc.valid with a custom validator, but I don't want to effectively store the document_id twice. I can find information on sending parameters to custom validators or creating Validation classes, but those all seem to do things like use Time.now or things like that -- dynamic information but not specific information related to the document. I could try to find the xml file based on the doc id in a custom validator, but my rake task or Controller already has this information, it just needs to hand it off to the Model to validate against. Theoretically this would either be a rake task or on the Controller, I can't imagine that mattering.
Thanks in advance.
How about using a virtual attribute to store the filename so your model has access?
Something like...
class Document < ActiveRecord::Base
attr_accessor :filename
validate :document_id_matches_filename
private
def document_id_matches_filename
errors.add(:document_id, 'does not match filename') unless self.document_id == File.basename(self.filename, '.xml')
end
end
That allows you to use ActiveModel's validations and get the error message you're looking for and encapsulate it all within the model without having to add an extra DB column.

Add http(s) to URL if it's not there?

I'm using this regex in my model to validate an URL submitted by the user. I don't want to force the user to type the http part, but would like to add it myself if it's not there.
validates :url, :format => { :with => /^((http|https):\/\/)?[a-z0-9]+([-.]{1}[a-z0-9]+).[a-z]{2,5}(:[0-9]{1,5})?(\/.)?$/ix, :message => " is not valid" }
Any idea how I could do that? I have very little experience with validation and regex..
Use a before filter to add it if it is not there:
before_validation :smart_add_url_protocol
protected
def smart_add_url_protocol
unless url[/\Ahttp:\/\//] || url[/\Ahttps:\/\//]
self.url = "http://#{url}"
end
end
Leave the validation you have in, that way if they make a typo they can correct the protocol.
Don't do this with a regex, use URI.parse to pull it apart and then see if there is a scheme on the URL:
u = URI.parse('/pancakes')
if(!u.scheme)
# prepend http:// and try again
elsif(%w{http https}.include?(u.scheme))
# you're okay
else
# you've been give some other kind of
# URL and might want to complain about it
end
Using the URI library for this also makes it easy to clean up any stray nonsense (such as userinfo) that someone might try to put into a URL.
The accepted answer is quite okay.
But if the field (url) is optional, it may raise an error such as undefined method + for nil class.
The following should resolve that:
def smart_add_url_protocol
if self.url && !url_protocol_present?
self.url = "http://#{self.url}"
end
end
def url_protocol_present?
self.url[/\Ahttp:\/\//] || self.url[/\Ahttps:\/\//]
end
Preface, justification and how it should be done
I hate it when people change model in a before_validation hook. Then when someday it happens that for some reason models need to be persisted with save(validate: false), then some filter that was suppose to be always run on assigned fields does not get run. Sure, having invalid data is usually something you want to avoid, but there would be no need for such option if it wasn't used. Another problem with it is that every time you ask from a model is it valid these modifications also take place. The fact that simply asking if a model is valid may result in the model getting modified is just unexpected, perhaps even unwanted. There for if I'd have to choose a hook I'd go for before_save hook. However, that won't do it for me since we provide preview views for our models and that would break the URIs in the preview view since the hook would never get called. There for, I decided it's best to separate the concept in to a module or concern and provide a nice way for one to apply a "monkey patch" ensuring that changing the fields value always runs through a filter that adds a default protocol if it is missing.
The module
#app/models/helpers/uri_field.rb
module Helpers::URIField
def ensure_valid_protocol_in_uri(field, default_protocol = "http", protocols_matcher="https?")
alias_method "original_#{field}=", "#{field}="
define_method "#{field}=" do |new_uri|
if "#{field}_changed?"
if new_uri.present? and not new_uri =~ /^#{protocols_matcher}:\/\//
new_uri = "#{default_protocol}://#{new_uri}"
end
self.send("original_#{field}=", new_uri)
end
end
end
end
In your model
extend Helpers::URIField
ensure_valid_protocol_in_uri :url
#Should you wish to default to https or support other protocols e.g. ftp, it is
#easy to extend this solution to cover those cases as well
#e.g. with something like this
#ensure_valid_protocol_in_uri :url, "https", "https?|ftp"
As a concern
If for some reason, you'd rather use the Rails Concern pattern it is easy to convert the above module to a concern module (it is used in an exactly similar way, except you use include Concerns::URIField:
#app/models/concerns/uri_field.rb
module Concerns::URIField
extend ActiveSupport::Concern
included do
def self.ensure_valid_protocol_in_uri(field, default_protocol = "http", protocols_matcher="https?")
alias_method "original_#{field}=", "#{field}="
define_method "#{field}=" do |new_uri|
if "#{field}_changed?"
if new_uri.present? and not new_uri =~ /^#{protocols_matcher}:\/\//
new_uri = "#{default_protocol}://#{new_uri}"
end
self.send("original_#{field}=", new_uri)
end
end
end
end
end
P.S. The above approaches were tested with Rails 3 and Mongoid 2.
P.P.S If you find this method redefinition and aliasing too magical you could opt not to override the method, but rather use the virtual field pattern, much like password (virtual, mass assignable) and encrypted_password (gets persisted, non mass assignable) and use a sanitize_url (virtual, mass assignable) and url (gets persisted, non mass assignable).
Based on mu's answer, here's the code I'm using in my model. This runs when :link is saved without the need for model filters. Super is required to call the default save method.
def link=(_link)
u=URI.parse(_link)
if (!u.scheme)
link = "http://" + _link
else
link = _link
end
super(link)
end
Using some of the aforementioned regexps, here is a handy method for overriding the default url on a model (If your ActiveRecord model has an 'url' column, for instance)
def url
_url = read_attribute(:url).try(:downcase)
if(_url.present?)
unless _url[/\Ahttp:\/\//] || _url[/\Ahttps:\/\//]
_url = "http://#{_url}"
end
end
_url
end
I had to do it for multiple columns on the same model.
before_validation :add_url_protocol
def add_url_protocol
[
:facebook_url, :instagram_url, :linkedin_url,
:tiktok_url, :youtube_url, :twitter_url, :twitch_url
].each do |url_method|
url = self.send(url_method)
if url.present? && !(%w{http https}.include?(URI.parse(url).scheme))
self.send("#{url_method.to_s}=", 'https://'.concat(url))
end
end
end
I wouldn't try to do that in the validation, since it's not really part of the validation.
Have the validation optionally check for it; if they screw it up it'll be a validation error, which is good.
Consider using a callback (after_create, after_validation, whatever) to prepend a protocol if there isn't one there already.
(I voted up the other answers; I think they're both better than mine. But here's another option :)

Friendly Form Validations (Rails)

I checked out both of these previously-asked questions, and they're a help but not a full solution for my case.
Essentially I need to validate a user-submitted URL from a form. I've started by validating that it begins with http://, https://, or ftp:// :
class Link < ActiveRecord::Base
validates_format_of [:link1, :link2, :link3,
:link4, :link5], :with => /^(http|https|ftp):\/\/.*/
end
That works great for what it's doing, but I need to go these two steps further:
Users should be allowed to leave the form fields blank if needed, and
If the URL provided by the user does not already start with http:// (say they enter google.com, for example), it should pass the validation but add the http:// prefix while being processed.
I'm having a hard time determining how to make this work cleanly and efficiently.
FYI, you don't have to pass an array to validates_format_of. Ruby will do arrays automagically (Rails parses the output of *args).
So, for your question, I'd go for something like this:
class Link < ActiveRecord::Base
validate :proper_link_format
private
def proper_link_format
[:link1, :link2, :link3, :link4, :link5].each do |attribute|
case self[attribute]
when nil, "", /^(http|https|ftp):\/\//
# Allow nil/blank. If it starts with http/https/ftp, pass it through also.
break
else
# Append http
self[attribute] = "http://#{self[attribute]}"
end
end
end
end
Just to add to the above, I use the Ruby URI module to parse URLs for validity.
http://www.ruby-doc.org/stdlib/libdoc/uri/rdoc/classes/URI.html
It works really well and it helps me to avoid regexes.

Resources