How to restrict values for a column - ruby-on-rails

I want to restrict available values for a field. So the value of the column must be from specified set of values. Is it possible using migration/models? Or I have to do it manually in my DB?

You'll use validations for this. There's a whole Rails guide on the topic. The specific helper you're looking for in this case is :inclusion, e.g.:
class Person < ActiveRecord::Base
validates :relationship_status,
:inclusion => { :in => [ 'Single', 'Married', 'Divorced', 'Other' ],
:message => "%{value} is not a valid relationship status" }
end
Edit Aug. 2015: As of Rails 4.1, you can use the enum class method for this. It requires that your column be an integer type:
class Person < ActiveRecord::Base
enum relationship_status: [ :single, :married, :divorced, :other ]
end
It automatically defines some handy methods for you, too:
p = Person.new(relationship_status: :married)
p.married? # => true
p.single? # => false
p.single!
p.single? # => true
You can read the documentation for enum here: http://api.rubyonrails.org/v4.1.0/classes/ActiveRecord/Enum.html

It depends on the amount of confidence you need. You could just add a validator to your model to restrict it to those values but then you wont be sure that existing data will match (and will cause subsequent saves to fail because of validation) and also that other changes could be made by other apps/raw sql that would get around it.
If you want absolute confidence, use the database.
Here's what you might want to use if you do it in the database (which is quite limited compared to what a rails validator could do: http://www.w3schools.com/sql/sql_check.asp

Related

Rails 4: Alternative to strings when setting "type" / "status" attribute?

I've run into this situation many times, where I need to store something like a status persay, so it would be something like this:
class Order < ActiveRecord::Base
INCOMPLETE = 'Incomplete'
IN_PROGRESS = 'In progress'
SHIPPED = 'Shipped'
CANCELLED = 'Cancelled'
...
end
Order would have a status attribute, and when creating an order I would just use collection_select with [INCOMPLETE, IN_PROGRESS, SHIPPED, CANCELLED] as the options.
Is there a cleaner way of doing this without using hardcoded strings, like using a Status association or some sort of PORO? I feel like this would be a bit brittle, like if someone changed the INCOMPLETE = 'Incomplete' to INCOMPLETE = 'Incompletezzzzz' then all the statuses of my existing records would not match.
I would advise you to use another model in this situation, something like OrderStatus:
class OrderStatus < ActiveRecord::Base
validates :internal_reference, presence: true, uniqueness: true
has_many :orders
def translated(locale = :en)
I18n.t("activerecord.attributes.order_status.#{self.internal_reference}", locale: locale, default: self.internal_reference)
end
class Order < ActiveRecord::Base
belongs_to :order_status
validates :order_status_id, presence: true
And the records will look like this:
OrderStatus.first
# => OrderStatus id: 1, internal_reference: canceled
Order.first
# => Order id: 3, order_status_id: 1 # etc.
In your views, it could be:
order.order_status.translated(I18n.locale)
# looks for activerecord.attributes.order_status.canceled
# if nothing found, returns the internal_reference, here it would return `'canceled'`
This configuration is better than just constants :
You can create as many statuses as you want,
You can translate them directly (using internal_reference as key for I18n),
The statuses can be tested either if they are in english, french or whatever (thanks to internal_reference,
You can create statuses directly in your app, without rebooting it,
You can set an attribute, like status_code and makes ranges (kind of like HTTP requests statuses) and group them (ex: if status_code > 100)
You can also add an boolean attribute cant_be_deleted to prevent from deleting Statuses used in the code.
You might think it is overkill to do so, but I guarantee that the day you will want to translate / add / remove / change your Statuses, it will be much easier with Models rather than Constants. Trust me, I worked for an Online shop, handling Carts, Orders and Products, I know how painful it is to change from constants to models, everywhere in your already existing code ;-)
I think enumerize gem can help
You can write your code like this
class Order < ActiveRecord:Base
extend Enumerize
enumerize :status, in: [:incomplete, :in_progress, :shipped, :cancelled]
end
It also works perfectly with I18n.
https://github.com/brainspec/enumerize

Ruby on Rails and NoSQL, adding fields

I'm just diving into Mongodb and MongoID with Rails and I find it awesome. One thing the NoSQL helps is when I can add extra fields to my model without any extra effort whenever I want:
class Page
include Mongoid::Document
include Mongoid::MultiParameterAttributes
field :title, :type => String
field :body, :type => String
field :excerpt, :type => String #Added later
field :location, :type => String #Added later
field :published_at, :type => Time
validates :title, :presence => true
validates :body, :presence => true
validates :excerpt, :presence => true
end
And this works perfectly as it should. But my question is, (sorry if this is trivial) the existing entries are blank and have no defined value for the newly added field. For example, in a sample blog application, after I've published two posts, I decide to add an excerpt and a location field to my database (refer code above). Any blog post that is published after the addition of these new fields can be made sure to have a value filled in for the excerpt field. But the posts published prior to the addition of these two new fields have null values (which is understandable why) which I cannot validate. Is there an elegant solution for this?
Thank you.
There are three basic options:
Update everything inside MongoDB to include the excerpt.
Use an after_initialize hook to add a default excerpt to existing objects when you pull them out of MongoDB.
Kludge your validation logic to only check for the existence of excerpt on new objects.
(1) requires a (possible large) time hit when you make the change but it is just a one time thing and you don't have to worry about it after that. You'd pull every Page out of MongoDB, do page.excerpt = 'some default excerpt', and then save it back to MongoDB. If you have a lot of Pages you'll want to process them in chunks of, say, 100 at a time. If you do this, you'll be able to search on the excerpt without worrying about what you should do with nulls. You can also do this inside MongoDB by sending a JavaScript fragment into MongoDB:
connection.eval(%q{
db.pages.find({}, { _id: true }).forEach(function(p) {
db.pages.update(
{ _id: p._id },
{ $set: { excerpt: 'some default excerpt' } }
);
});
})
(2) would go something like this:
after_initialize :add_default_excerpt, :unless => :new_record?
#...
private
def add_default_excerpt
self.excerpt = 'some default excerpt' unless self.excerpt.present?
end
You could move the unless self.excerpt up to the :unless if you didn't mind using a lambda:
after_initialize :add_default_excerpt, :unless => ->{ |o| o.new_record? || o.excerpt.present? }
#...
private
def add_default_excerpt
self.excerpt = 'some default excerpt'
end
This should be pretty quick and easy to set up but there are downsides. First of all, you'd have a bunch of nulls in your MongoDB that you might have to treat specially during searches. Also, you'd be carrying around a bunch of code and logic to deal with old data but this baggage will be used less and less over time. Furthermore, the after_initialize calls do not come for free.
(3) requires you to skip validating the presence of the excerpt for non-new Pages (:unless => :new_record?) or you'd have to find some way to differentiate new objects from old ones while also properly handling edits of both new and old Pages. You could also force people to supply an excerpt when they change a Page and leave your validation as-is; including a :default => '' on your field :excerpt would take care of any nil issues in views and such.
I'd go with (1) if possible. If the update would take too long and you wanted the site up and running while you were fixing up MongoDB, you could add a :default => '' while updating and then remove the :default option, restart, and manually patch up any strays that got through.

Validations that rely on associations being built in Rails

A Course has many Lessons, and they are chosen by the user with a JS drag-n-drop widget which is working fine.
Here's the relevant part of the params when I choose two lessons:
Parameters: {
"course_lessons_attributes"=>[
{"lesson_id"=>"43", "episode"=>"1"},
{"lesson_id"=>"44", "episode"=>"2"}
]
}
I want to perform some validations on the #course and it's new set of lessons, including how many there are, the sum of the lessons' prices and other stuff. Here's a sample:
Course Model
validate :contains_lessons
def contains_lessons
errors[:course] << 'must have at least one lesson' unless lessons.any?
end
My problem is that the associations between the course and the lessons are not yet built before the course is saved, and that's when I want to call upon them for my validations (using course.lessons).
What's the correct way to be performing custom validations that rely on associations?
Thanks.
looks like you don't need a custom validation here, consider using this one:
validates :lessons, :presence => true
or
validates :lessons, :presence => {:on => :create}
You can't access the course.lessons, but the course_lessons are there, so I ended up doing something like this in the validation method to get access to the array of lessons.
def custom validation
val_lessons = Lesson.find(course_lessons.map(&:lesson_id))
# ...
# check some things about the associated lessons and add errors
# ...
end
I'm still open to there being a better way to do this.

How to ignore duplicate inserts with datamapper

I have a datamapper model that has a unique index on a property called name. I want to create new records when a name doesn't already exist and silently ignore attempts to create records with duplicate names. What's the "right" way to do this in datamapper?
The best approach is to use the dm-validations gem, and ensure that your name property is specified as being unique, eg:
class Committer
include DataMapper::Resource
# ... other properties ...
property :name, String, :length => 1..100, :required => true, :unique => true
end
The dm-validations gem will introspect your model and automatically setup validations for your properties. In this case it won't allow more than one Committer to have the same name.
I think the best answer is to use first_or_create, which as Dan points out above, is already built into datamapper and therefore doesn't need to be declared.
require 'dm-core'
require 'dm-validations'
class Committer
include DataMapper::Resource
property :id, Serial
property :name, String, :unique_index => true
validates_present :name
validates_is_unique :name
end
committer = "George"
record = Committer.first_or_create(:name => committer)
One solution I came up with is simply to ignore the exception:
begin
Committer.create!(:name => committer)
rescue DataObjects::IntegrityError => e # ignore duplicate inserts
end
If you have a better (more idiomatic) way of doing it, please let me know.
Here is another solution I came up with:
require 'dm-core'
require 'dm-validations'
require 'dm-more'
record = Committer.find_or_create(:name => committer)
If you're using this in sinatra, requiring dm-more seems to cause
other problems. My solution was to require my own file that only
contained the following code:
module DataMapper
module Model
def first_or_create(conditions = {}, attributes = {})
first(conditions) || create(conditions.merge(attributes))
end
alias find_or_create first_or_create
end
end

How do I define synthetic attributes for an ActiveRecord model?

I have an ActiveRecord model whose fields mostly come from the database. There are additional attributes, which come from a nested serialised blob of stuff. This has been done so that I can use these attributes from forms without having to jump through hoops (or so I thought in the beginning, anyway) while allowing forwards and backwards compatibility without having to write complicated migrations.
Basically I am doing this:
class Licence < ActiveRecord::Base
attr_accessor :load_worker_count
strip_attributes!
validates_numericality_of :load_worker_count,
:greater_than => 2, :allow_nil => true, :allow_blank => true
before_save :serialise_fields_into_properties
def serialise_fields_into_properties
...
end
def after_initialize
...
end
...
end
The problem I noticed was that I can't get empty values in :load_worker_count to be accepted by the validator, because:
If I omit :allow_blank, it fails validation complaining about it being blank
If I put in :allow_blank, it converts the blank to 0, which when fails on the :greater_than => 2
In tracking down why these blank values are getting to the validation stage in the first place, I discovered the root of the problem: strip_attributes! only affects actual attributes, as returned by the attributes method. So the values which should be nil at time of validation are not. So it feels like the root cause is that the synthetic attributes I added in aren't seen when setting which attributes to strip, so therefore I ask:
Is there a proper way of creating synthetic attributes which are recognised as proper attributes by other code which integrates with ActiveRecord?
I assume you are talking of the strip_attributes plugin; looking at the code, it uses the method attributes, defined in active_record/base.rb, which uses #attributes, which is initialized (in initialize) as #attributes = attributes_from_column_definition.
Maybe it's possible to hack ActiveRecord::Base somehow, but it would be a hard work: #attributes is also used when getting/putting stuff from/to db, so you would have to do a lot of hacking.
There's a much simpler solution:
before_validate :serialise_fields_into_properties
...
def serialise_fields_into_properties
if load_worker_count.respond_to? :strip
load_worker_count = load_worker_count.blank? ? nil : load_worker_count.strip
end
...
end
After all, this is what strip_attributes! does.
Wouldn't it be easier to just use Rails' serialize macro here?
class License < ActiveRecord::Base
serialize :special_attributes
end
Now you can assign a hash or array or whatever you need to special_attributes and Rails will serialize it a text field in the database.
license = License.new
license.special_attributes = { :beer => true, :water => false }
This will keep your code clean and you don't have to worry about serializing/deserializing attributes yourself.

Resources