How to validate a mongoid field based on another mongoid field? - ruby

I made the switch to nosql and am playing with Mongoid with Sinatra. I'm still new to this, and I"m stumped on this problem.
class Item
field :name, type: String
field :category, type: Moped::BSON::ObjectId # holds _id of the category object
field :cover_type, type: String
validates_presence_of :category
validates_inclusion_of :cover_type, in:["Hardcover", "Softcover"]
end
class Category
field :name
validates_inclusion_of :name, in:["Books", "Movies", "Ebooks"]
end
Imagine a store that sells Books, Movies, and Ebooks and each item sold belongs to one of the three categories. If an Item is listed under the category "Books", then the Item is required to have a field called cover_type. Furthermore, cover_type can only be either "Hardcover" or "Softcover".
When saving an Item, how do I piece together the validate in the Item class for when the Item is in the Book category and therefore requires the presence of the field cover_type, which is also validated as being "Hardcover" or "Softcover"?
If the Item isn't a Book, then cover_type can be null.

Mongoid models inherit ActiveModel validations. You can just write a custom validator method:
class Item
validates :cover_type_must_be_valid, if: :book?
def book?
Category.find(category).name == 'Books'
end
def cover_type_must_be_valid
errors.add(:cover_type, 'must be Hardcover or Softcover') unless %w{Hardcover Softcover}.include? cover_type
end
end
The book? method is unpleasant; why not use belongs_to :category on Item and has_many :items on Category?
EDIT: here’s what book? could look like if you used the has_many and belongs_to:
def book?
category.name == 'Books'
end
Not that different, but you’ll surely be accessing item.category all over your application, I can’t see why you wouldn’t want to make it easier.

Related

Ruby datamapper associations

I am just learning Ruby and datamapper, I have read the docs about associations from the official DataMapper site, but I still have two problems.
First whenever I add associated object, I can not see it when displaying all objects.
I have test class like:
class Test
include DataMapper::Resource
property :id, Serial
property :name, String
has 1, :phonen, :through => Resource
end
And then phonen class like:
class Phonen
include DataMapper::Resource
property :id, Serial
property :number, String
belongs_to :test
end
Then I am creating those 2 objects
#test = Test.create(
:name => "Name here"
)
#phone = Phonen.create(
:number => "Phone number"
)
#test.phonen = #phone
#test.save
And I want to display them like that (I want to return json)
get '/' do
Test.all.to_json
end
What am I doing wrong? maybe its something with the to_json...
I honestly don't know..
But I have one additional question to this topic, lets say I managed to connect those two classes, if I display JSON will I get Phonen { } or just inside class { }?
I know its probably very easy question, but I can't figure it out. That's why I decided to ask you guys. Thanks for help
Test.all
Is returning an active record association in array form, not a hash, when you try to convert to json it's failing.
You can try:
render json: Test.all
As asked in this question:
Ruby array to JSON and Rails JSON rendering

FactoryGirl.create doesn't save Mongoid relations

I have two classes:
class User
include Mongoid::Document
has_one :preference
attr_accessible :name
field :name, type: String
end
class Preference
include Mongoid::Document
belongs_to :user
attr_accessible :somepref
field :somepref, type: Boolean
end
And I have two factories:
FactoryGirl.define do
factory :user do
preference
name 'John'
end
end
FactoryGirl.define do
factory :preference do
somepref true
end
end
After I create a User both documents are saved in the DB, but Preference document is missing user_id field and so has_one relation doesn't work when I read User from the DB.
I've currently fixed it by adding this piece of code in User factory:
after(:create) do |user|
#user.preference.save! #without this user_id field doesn't get saved
end
Can anyone explain to me why is this happening and is there a better fix?
Mongoid seems to be lacking support here.
When FactoryGirl creates a user, it first has to create the preference for that new user. As the new user does not have an id yet, the preference can't store it either.
In general, when you try create parent & child models in one operation, you need two steps:
create the parent, persist to database so it get's an id.
create the child for the parent and persist it.
Step two would end up in an after(:create) block. Like this:
FactoryGirl.define do
factory :user do
name 'John'
after(:create) do |user|
preference { create(:preference, user: user) }
end
end
end
As stated in this answer:
To ensure that you can always immediately read back the data you just
wrote using Mongoid, you need to set the database session options
consistency: :strong, safe: true
neither of which are the default.

Dropdown Filter for Polymorphic association nested_attribute

I'm building an application in which the user can create a Company and add Investments made to that company. The investments can come from two sources user's Funds or companies' Coinvestors. Funds are big deal in the application as the user can do a bunch of stuff in them. The Coinvestors are not that important, but I want to have control over few aspects of so I created a Model just for them. For that I created a Polymorphic association for which I gave the [terrible] name of Investables. I'm running Rails 3.2.15 and Ruby 2.0.0. Models are below:
class Company < ActiveRecord::Base
has_many :investments
accepts_nested_attributes_for :investments
end
class Investment < ActiveRecord::Base
belongs_to :fund, :class_name => "Fund", :foreign_key => 'investable_id'
belongs_to :company, inverse_of: :investments
belongs_to :coinvestor, :class_name => "Coinvestor", :foreign_key => 'investable_id'
end
class Fund < ActiveRecord::Base
has_many :investments, :as => :investable, :dependent => :destroy
end
class Coinvestor < ActiveRecord::Base
has_many :investments, :as => :investable, :dependent => :destroy
end
When editing the company I want to be able to add investments and I want to dynamically add form lines for each new investment. I was able to achieve that following the awesome 165-Edit Multiple Revised.
To make it more complex, I also want to add a Dropdown for choosing the Polymorphic Type so it filters the next Dropdown to show only the names of either the Funds or the Coinvestors.
For that I mostly adapted code from Railscast 88-Dynamic Select Menus(Thanks Ryan!!)
/views/company/edit.html.erb
<%= form_for(#company) do |f| %>
...
<%= f.fields_for :investments do |builder| %>
<%= render 'investment_fields', f: builder %>
<% end %>
<%= link_to_add_fields "Add Investment", f, :investments, 'table' %>
/views/company/_investment_fields.html.erb
<%= f.select :investable_type , [ "Fund", "Coinvestor" ], {prompt: "Investor Type"} %>
<%= f.grouped_collection_select( :investable_id, investables_to_collection, :investables, :name, :id, :name, {prompt: "Investor"} ) %>
The "investable_to_collection" is a helper I built to aggregate objects from both Funds and Coinvestors models.
module CompaniesHelper
InvestableCollection = Struct.new(:name, :investables)
CollectionItem = Struct.new(:name, :id)
def investables_to_collection
a = Array.new
a << InvestableCollection.new('Fund', Fund.all.map { |item| CollectionItem.new(item.name, item.id )})
a << InvestableCollection.new('Coinvestor', Coinvestor.all.map { |item| CollectionItem.new(item.name, item.id )})
a
end
end
I didn't add any JavaScript yet to filter the dropdown which will be another challenge. But I've got my beautiful view to show data I've already got in the DB. But the dropdown that should show the Fund's or Coinvestor's name is mixing up things: it will show the name of Coinvestor with ID == 1 even if the investment was made by a Fund.
I thought of making one of the models to have a custom ID such as f1, f2, f3 ... instead of 1, 2, 3... so the system wouldn't mix them. But it seems that it would generate other big compatibility issues.
Do you guys have any other idea?
I wouldn't alter the id column (i.e. I wouldn't change it from an auto-incrementing integer), but because you're mixing two lists in the DB into a single list in the UI you will need some way to note which table each item in the UI came from. For that, sure, use whatever ID scheme you want and then use that differentiating ID as the way to lookup the relevant item.
Also, if you're not going to use the auto-incrementing id columns in the UI (for links or whatever), then you could also just remove them and replace them with your custom identification scheme. There's no reason, for example, that you couldn't assign them all a random 8-digit number, ensuring that number is unique across the different types you're going to put into the list, and then using that id in the UI. It seems that the real issue you're running into is how to combine the lists where ids might overlap, and perhaps it may be feasible for you to devise a way to assign a non-overlapping ID.
Another possibility for assigning non-overlapping IDs, without having to come up with your own scheme or checking for uniqueness across multiple tables is to use a UUID for the lookup ID in the UI drop-down.

How to define method names and object references before actually having to use them (Ruby)

Say I am keeping track of email correspondances. An enquiry (from a customer) or a reply (from a supporter) is embedded in the order the two parties are corresponding about. They share the exact same logic when put into the database.
My problem is that even though I use the same logic, the object classes are different, the model fields I need to call are different, and the method names are different as well.
How do I put methods and objects references in before I actually have to use them? Does a "string_to_method" method exists or something like that?
Sample code with commentaries:
class Email
include Mongoid::Document
field :from, type: String
field :to, type: String
field :subject, type: String
belongs_to :order, :inverse_of => :emails
def start
email = Email.create!(:from => "sender#example.com", :to => "recipient#example.com", :subject => "Hello")
from_or_to = from # This represents the database field from where I later on will fetch the customers email address. It is either from or to.
enquiries_or_replies = enquiries # This represents a method that should later be called. It is either enquiries or replies.
self.test_if_enquiry_or_reply(from_or_to, enquiries_or_replies)
end
def test_if_enquiry_or_reply(from_or_to, enquiries_or_replies)
order = Order.add_enquiry_or_reply(self, from_or_to, enquiries_or_replies)
self.order = order
self.save
end
end
class Order
include Mongoid::Document
field :email_address, type: String
has_many :emails, :inverse_of => :order
embeds_many :enquiries, :inverse_of => :order
embeds_many :replies, :inverse_of => :order
def self.add_enquiry_or_reply(email, from_or_to, enquiries_or_replies)
order = Order.where(:email_address => email.from_or_to).first # from_or_to could either be from or to.
order.enquiries_or_replies.create!(subject: email.subject) # enquiries_or_replies could either be enquiries or replies.
order
end
end
Judging by the question and the code sample, it sounds like you are mixing concerns too much. My first suggestion would be to re-evaluate your method names and object structure. Ambiguous names like test_if_thing1_or_thing2 and from_or_to (it should just be one thing) will make it very hard for others, and your future self, to understand the code laster.
However, without diverging into a debate on separation of concerns, you can change the methods you call by using public_send (or the private aware send). So you can do
order.public_send(:replies).create!
order.public_send(:enquiries).create!
string to method does exist, it's called eval
so, you could do
method_name = "name"
eval(method_name) #calls the name method

How to show model title instead on #<Mymodel:0x000000...> in activeadmin dropdown menus?

I've created an association where Project has_many Tasks and Task belongs_to Project.
I've created the form in admin/tasks.rb
form do |f|
f.inputs "Details" do
f.input :title
f.input :project
end
f.buttons
end
Now in the edit Task page I hahe a dropdown menu where I can choose the project, but the entry are #<Project:0x00000...>.
How can I customize the entries in the dropdown to show the Project title field instead?
I'm a Rails newbie.
Active admin makes use of formtastic, under the hood formtastic loops through your model searching for a method like name, to_s, value, title, that returns a string.
At the moment you see the data entry itself, if you want formtastic to show the name, make sure you put something like
def name
return self.what_key_you_want_to_use
end
in your Project.rb model.
That should let formtastic show the name action instead of the model .to_s!
This solved it for me:-
In project.rb (Model) to make ActiveAdmin display properly in select dropdown use alias_attribute.
alias_attribute :name, :project_name (or whatever you named the field in your database)
tldr: You can define or alias :to_label on your model to customize the label used:
def to_label
"#{name} - (#{id})"
end
alias_attribute :to_label, :name
The library used by Rails: Formtastic, (or an alternative: Simple Form) uses the collection_label_methods to configure which fields are checked for deriving a label for your model.
Formastic defaults are: "to_label", "display_name", "full_name", "name", "title", "username", "login", "value", "to_s"
Simple Form defaults are: :to_label, :name, :title, :to_s
As most of these fields might already be used in your model, to_label or display_name seems to be the good candidates. I prefer to_label.
You can create a proc like such :
f.input :your_field, member_label: Proc.new { |p| "#{p.name}"}

Resources