I'm trying to think of a best solution for following scenario. I've a model called an 'Article' with an integer field called 'status'. I want to provide class level array of statuses as shown below,
class Article < ActiveRecord::Base
STATUSES = %w(in_draft published canceled)
validates :status, presence: true
validates_inclusion_of :status, :in => STATUSES
def status_name
STATUSES[status]
end
# Status Finders
def self.all_in_draft
where(:status => "in_draft")
end
def self.all_published
where(:status => "published")
end
def self.all_canceled
where(:status => "canceled")
end
# Status Accessors
def in_draft?
status == "in_draft"
end
def published?
status == "published"
end
def canceled?
status == "canceled"
end
end
So my question is if this is the best way to achieve without having a model to store statuses? And secondly how to use these methods in ArticlesController and corresponding views? I'm struggling to understand the use of these methods. To be specific, how to do following?
article = Article.new
article.status = ????
article.save!
or
<% if article.in_draft? %>
<% end %>
I greatly appreciate any sample code example. I'm using rails 4.0.0 (not 4.1.0 which has enum support).
You could define all the methods using define_method, and use a hash instead of an array:
STATUSES = {:in_draft => 1, :published => 2, :cancelled => 3}
# Use the values of the hash, to validate inclusion
validates_inclusion_of :status, :in => STATUSES.values
STATUSES.each do |method, val|
define_method("all_#{method)") do
where(:status => method.to_s)
end
define_method("#{method}?") do
self.status == val
end
end
In that way, you can add statuses in the future without needing to create the methods manually. Then you can do something like:
article = Article.new
article.status = Article::STATUSES[:published]
...
article.published? # => true
Related
How to use model's properties at Rubi on Rails' views?
My model:
class EntryItems < ActiveRecord::Base
attr_accessible :created_on, :updated_on, :activity_type, :date_from, :date_to
attr_protected :user_id, :from_tyear, :from_tmonth, :from_tday
##date_from = nil
def date_from
##date_from = Date.new(read_attribute(:from_tyear), read_attribute(:from_tmonth), read_attribute(:from_tday))
end
def date_from=(date_from_value)
if date_from_value.is_a?(Time)
##date_from = date_from_value.to_date
end
write_attribute(:from_tyear, date_from_value.year)
write_attribute(:from_tmonth, date_from_value.mon)
write_attribute(:from_tday, date_from_value.mday)
end
end
My controller:
class ItemEntriesSetupController < ApplicationController
unloadable
def index
#item_entry = DayoffEntries.new(:user => User.current, :created_on => Time.now, :updated_on => Time.now)
#item_entry_post_url = url_for(:controller => 'item_entries_setup', :action => 'update')
end
def update
#code is skipped
end
end
My view:
<%= form_tag(#item_entry_post_url) do %>
<dl>
<label>Issues:</label>
<%=date_field_tag 'date_from', true, #item_entry.date_from %>
</dl>
<%= submit_tag(l(:button_create)) %>
<% end %>
It fails with underfined method 'div' for nil:NilClass.
What do I do wrong?
(I'm really new in Ruby on Rails, so I still have problems with finding right practicies, because there're plenty of code examples written n different style).
I have following ugly create_unique method in few models ex:
def self.create_unique(p)
s = Subscription.find :first, :conditions => ['user_id = ? AND app_id = ?', p[:user_id], p[:app_id]]
Subscription.create(p) if !s
end
And then in controllers #create actions I have
s = Subscription.create_unique({:user_id => current_user.id, :app_id => app.id})
if s
raise Exceptions::NotAuthorized unless current_user == s.user
#app = s.app
s.destroy
flash[:notice] = 'You have been unsubscribed from '+#app.name+'.'
redirect_to '/'
end
did you try dynamic finders ?
find_or_initialize_by_user_id_and_app_id
find_or_create_by_user_id_and_app_id
first_or_initialize...
first_or_create....
check manual http://guides.rubyonrails.org/active_record_querying.html#dynamic-finders
also option is to create validation rule for check unique value
class Subscription < ActiveRecord::Base
validates_uniqueness_of :user_id, :scope => :app_id
end
then
sub = Subscription.new({:user_id => current_user.id, :app_id => app.id})
sub.valid? #false
You can use validates_uniquness_of :app_id,:scope=>:user_id so app id is uniq for respected user_id
I'd like to make full use of the organic character of a NoSQL document and build a dynamic data model which can grow, be changed, and is different for most datasets. Below is the model SomeRequest.rb with the code to set and get from Couchbase, but I can't get the function addOrUpdate(key, value) to work:
undefined method `each' for "0":String
Completed 500 Internal Server
Error in 16ms NoMethodError (undefined method `each' for "0":String):
config/initializers/quiet_assets.rb:7:in `call_with_quiet_assets'
Is the returning error. Is there a way to make this work, to add (or update existing) keys and save the document to the database afterwards?
class SomeRequest < Couchbase::Model
include ActiveModel::Validations
include ActiveModel::Conversion
extend ActiveModel::Callbacks
extend ActiveModel::Naming
# Couch Model
define_model_callbacks :save
attribute :session_id
attribute :views, :default => 0
attribute :created_at, :default => lambda { Time.zone.now }
# iterate through attr keys and set instance vars
def initialize(attr = {})
#errors = ActiveModel::Errors.new(self)
unless attr.nil?
attr.each do |name, value|
setter = "#{name}="
next unless respond_to?(setter)
send(setter, value)
end
end
end
def addOrUpdate(key, value)
self[key] = value
end
def save
return false unless valid?
run_callbacks :save do
Couch.client.set(self.session_id, self)
end
true
end
def self.find(key)
return nil unless key
begin
doc = Couch.client.get(key)
self.new(doc)
rescue Couchbase::Error::NotFound => e
nil
end
end
end
Why don't you like to use find, save and create methods from couchbase-model gem?
class Couchbase::Error::RecordInvalid < Couchbase::Error::Base
attr_reader :record
def initialize(record)
#record = record
errors = #record.errors.full_messages.join(", ")
super("Record Invalid: #{errors}")
end
end
class SomeRequest < Couchbase::Model
include ActiveModel::Validations
attribute :session_id
attribute :views, :default => 0
attribute :created_at, :default => lambda { Time.zone.now }
validates_presence_of :session_id
before_save do |doc|
if doc.valid?
doc
else
raise Couchbase::Error::RecordInvalid.new(doc)
end
end
def initialize(*args)
#errors = ActiveModel::Errors.new(self)
super
end
end
And you might be right, it worth to add validation hooks by default, I think I will do it in next release. The example above is valid for release 0.3.0
What considering updateOrAdd I recommend you just use method #save and it will check if the key is persisted (currently by checking id attribute) and if the record doesn't have key yet, it will generate key and update it.
Update
In version 0.4.0 I added validation hooks into the gem, so the example above could be rewritten simpler.
class SomeRequest < Couchbase::Model
attribute :session_id
attribute :views, :default => 0
attribute :created_at, :default => lambda { Time.zone.now }
validates_presence_of :session_id
end
I have this model:
class Campaign
include Mongoid::Document
include Mongoid::Timestamps
field :name, :type => String
field :subdomain, :type => String
field :intro, :type => String
field :body, :type => String
field :emails, :type => Array
end
Now I want to validate that each email in the emails array is formatted correctly. I read the Mongoid and ActiveModel::Validations documentation but I didn't find how to do this.
Can you show me a pointer?
You can define custom ArrayValidator. Place following in app/validators/array_validator.rb:
class ArrayValidator < ActiveModel::EachValidator
def validate_each(record, attribute, values)
Array(values).each do |value|
options.each do |key, args|
validator_options = { attributes: attribute }
validator_options.merge!(args) if args.is_a?(Hash)
next if value.nil? && validator_options[:allow_nil]
next if value.blank? && validator_options[:allow_blank]
validator_class_name = "#{key.to_s.camelize}Validator"
validator_class = begin
validator_class_name.constantize
rescue NameError
"ActiveModel::Validations::#{validator_class_name}".constantize
end
validator = validator_class.new(validator_options)
validator.validate_each(record, attribute, value)
end
end
end
end
You can use it like this in your models:
class User
include Mongoid::Document
field :tags, Array
validates :tags, array: { presence: true, inclusion: { in: %w{ ruby rails } }
end
It will validate each element from the array against every validator specified within array hash.
Milovan's answer got an upvote from me but the implementation has a few problems:
Flattening nested arrays changes behavior and hides invalid values.
nil field values are treated as [nil], which doesn't seem right.
The provided example, with presence: true will generate a NotImplementedError error because PresenceValidator does not implement validate_each.
Instantiating a new validator instance for every value in the array on every validation is rather inefficient.
The generated error messages do not show why element of the array is invalid, which creates a poor user experience.
Here is an updated enumerable and array validator that addresses all these issues. The code is included below for convenience.
# Validates the values of an Enumerable with other validators.
# Generates error messages that include the index and value of
# invalid elements.
#
# Example:
#
# validates :values, enum: { presence: true, inclusion: { in: %w{ big small } } }
#
class EnumValidator < ActiveModel::EachValidator
def initialize(options)
super
#validators = options.map do |(key, args)|
create_validator(key, args)
end
end
def validate_each(record, attribute, values)
helper = Helper.new(#validators, record, attribute)
Array.wrap(values).each do |value|
helper.validate(value)
end
end
private
class Helper
def initialize(validators, record, attribute)
#validators = validators
#record = record
#attribute = attribute
#count = -1
end
def validate(value)
#count += 1
#validators.each do |validator|
next if value.nil? && validator.options[:allow_nil]
next if value.blank? && validator.options[:allow_blank]
validate_with(validator, value)
end
end
def validate_with(validator, value)
before_errors = error_count
run_validator(validator, value)
if error_count > before_errors
prefix = "element #{#count} (#{value}) "
(before_errors...error_count).each do |pos|
error_messages[pos] = prefix + error_messages[pos]
end
end
end
def run_validator(validator, value)
validator.validate_each(#record, #attribute, value)
rescue NotImplementedError
validator.validate(#record)
end
def error_messages
#record.errors.messages[#attribute]
end
def error_count
error_messages ? error_messages.length : 0
end
end
def create_validator(key, args)
opts = {attributes: attributes}
opts.merge!(args) if args.kind_of?(Hash)
validator_class(key).new(opts).tap do |validator|
validator.check_validity!
end
end
def validator_class(key)
validator_class_name = "#{key.to_s.camelize}Validator"
validator_class_name.constantize
rescue NameError
"ActiveModel::Validations::#{validator_class_name}".constantize
end
end
You'll probably want to define your own custom validator for the emails field.
So you'll add after your class definition,
validate :validate_emails
def validate_emails
invalid_emails = self.emails.map{ |email| email.match(/^([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})$/i) }.select{ |e| e != nil }
errors.add(:emails, 'invalid email address') unless invalid_emails.empty?
end
The regex itself may not be perfect, but this is the basic idea. You can check out the rails guide as follows:
http://guides.rubyonrails.org/v2.3.8/activerecord_validations_callbacks.html#creating-custom-validation-methods
Found myself trying to solve this problem just now. I've modified Tim O's answer slightly to come up with the following, which provides cleaner output and more information to the errors object that you can then display to the user in the view.
validate :validate_emails
def validate_emails
emails.each do |email|
unless email.match(/^([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})$/i)
errors.add(:emails, "#{email} is not a valid email address.")
end
end
end
Here's an example that might help out of the rails api docs: http://apidock.com/rails/ActiveModel/Validations/ClassMethods/validates
The power of the validates method comes when using custom validators and default validators in one call for a given attribute e.g.
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors[attribute] << (options[:message] || "is not an email") unless
value =~ /\A([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
end
end
class Person
include ActiveModel::Validations
attr_accessor :name, :email
validates :name, :presence => true, :uniqueness => true, :length => { :maximum => 100 }
validates :email, :presence => true, :email => true
end
class Api::StoresController < ApplicationController
respond_to :json
def index
#stores = Store.all(:include => :products)
respond_with #stores
end
end
Returns only stores without their products, as does
Store.find(:all).to_json(:include => :products)
The association is tested, I can see the nested products in console ouput from, say,
Store.first.products
What's the correct way to get them products included with MongoMapper?
Here are my models:
class Store
include MongoMapper::Document
many :products, :foreign_key => :store_ids
end
class Product
include MongoMapper::Document
key :store_ids, Array, :typecast => 'ObjectId'
many :stores, :in => :store_ids
end
UPDATE
In trying Scott's suggestion, I've added the following to the Store model:
def self.all_including_nested
stores = []
Store.all.each do |store|
stores << store.to_hash
end
end
def to_hash
keys = self.key_names
hash = {}
keys.each{|k| hash[k] = self[k]}
hash[:products] = self.products
hash[:services] = self.services
hash
end
And in the controller:
def index
#stores = Store.all_including_nested
respond_with #stores
end
Which looks like it should work? Assuming the array of hashes would have #to_json called on it, and then the same would happen to each hash and each Product + Service. I'm reading through ActiveSupport::JSON's source, and so far that's what I've grokked from it.
But, not working yet... :(
Have a look at the as_json() method. You put this in your models, define your json, and then simply call the render :json method and get what you want.
class Something
def as_json(options={})
{:account_name => self.account_name,
:expires_on => self.expires_on.to_s,
:collections => self.collections,
:type => "Institution"}
end
end
You'll notice self.collections which is a many relationship. That model also has as_json() defined:
class Collection
def as_json(options={})
{:name => self.name,
:title => self.title,
:isbn => self.isbn,
:publisher => self.publisher,
:monthly_views => self.monthly_views}
end
end
This one contains self.monthly_views which represents another many relationship.
Then in your controller:
#somethings = Something.all
render :json => #somethings
You might have to create your own method to generate a hash then turn the hash into JSON. I'm thinking something like this:
store = Store.first
keys = store.key_names
hash = {}
keys.each{|k| hash[k] = store[k]}
hash[:products] = store.products
hash.to_json