I'm trying to create an object with embedded attributes for testing. This example uses a user with multiple languages but I would love a solution for the general case of creating embedded objects.
Present creation code:
def valid_attributes
{ :languages => [Language.new(language: "en-US", proficiency: "1")] }
end
user = User.create! valid_attributes
The models:
class User
include Mongoid::Document
field :languages
embeds_many :languages
validates_presence_of :languages
attr_accessible :languages_attributes
accepts_nested_attributes_for :languages, :reject_if => lambda { |a| a[:language].blank? }, :allow_destroy => true
end
class Language
include Mongoid::Document
field :language
field :proficiency
key :language
embedded_in :user
attr_accessible :language, :proficiency
end
These models work fine for creating objects from nested forms in Ryan Bates' footsteps (https://github.com/ryanb/complex-form-examples). I don't know whether that is the right way, but I assume so.
There are a few obvious solutions I can see. One is to just hardcode the input like what is generated from the forms:
{"user"=>{ "languages_attributes"=>{"0"=>{"language"=>"en-US", "proficiency"=>"1", "_destroy"=>"false", "id"=>"en-dash-us"}}}
That doesn't seem DRY or sane in the long run, to me.
The other solution is to just cut the embedded objects and use Arrays. Mongoid is pretty good at supporting arrays but you lose the ability to write validations for each object and the code would be less reusable.
Thoughts, Stackoverflowers?
For what it's worth, I followed #cug's advice and used Fabrication. I'm posting my code here for the benefit of others who hit this issue.
The spec/user/fabricator.rb Fabricator
Fabricator(:language) do
language "en-US"
proficiency "1"
end
Fabricator(:user) do
languages { [ Fabricate.build(:language, :language => "en-US", :proficiency => "1") ] }
end
Creating a user like this:
user = Fabricate.build(:user)
Thanks all, case closed. ^^
You should really use Fixtures, or better Factories for that. FactoryGirl is very popular
for creating factories.
In your case you will have to define the factory in spec/factories.rb like that:
FactoryGirl.define do
factory :user do
languages { [association(:language)] }
end
factory :language do
language "en-US"
proficiency "1"
end
end
And than use it in your tests like that
user = FactoryGirl.create :user
Related
I'm having a tough time figuring something out in Rails. It probably has to do with my very limited knowledge of SQL, since I know Rails pretty well. I'm using Rails 5.
I have two models: Applicant and Application.
class Applicant < ApplicationRecord
has_one :application
has_many :skills
accepts_nested_attributes_for :application
accepts_nested_attributes_for :skills,
reject_if: ->(skill) { skill[:name].empty? || skill[:experience].empty? }
validates_with ApplicantValidator
end
class Application < ApplicationRecord
belongs_to :applicant
has_many :notes
VALID_STATUSES = ["in review", "accepted", "declined", "closed"]
validates_length_of :why_interested, minimum: 25
validates :accept_terms, acceptance: true
validates :status, inclusion: { in: VALID_STATUSES }
before_validation :set_status
private
def set_status
self.status ||= "in review"
end
end
I'd like to add a scope, :active, to the Applicant model that returns only applicants who have an application whose status is "in review". However, I can't find a way to access the application within a scope proc.
I've seen other suggestions for cases where there is a has_many relationship with the child, but they didn't work in my case.
I doubt it makes a difference, but I'm using Postgres. The closest I've come to a solution is to add this, but when I run RSpec it says there needs to be a FROM-clause for the applications table. I don't know how to effect that.
scope :active, -> { joins(:application).where('"application"."status" = "in review"') }
scope :in_review_applicants, -> { joins(:application).where('application.status = ?', :in_review) }
I think is something like that..
I have a embeds_many association and when I save the parent document the children are not being saved.
class User
include Mongoid::Document
embeds_many :user_missions
attr_accessible :user_missions_attributes
accepts_nested_attributes_for :user_missions, allow_destroy: true
end
class UserMission
include Mongoid::Document
embedded_in :user, :inverse_of => :user_missions
has_one :mission, autosave: true
validates_presence_of :mission
attr_accessible :mission_title
def mission_title
mission.try(:title)
end
def mission_title=(title)
self.mission = Mission.find_or_create_by(:title => title) if title.present?
end
end
Here is the spec I'm failing:
it "should save mission to user_mission when created" do
user_mission = UserMission.new
user = create(:user)
user.user_missions << user_mission
user_mission.mission_title = "Created Title"
user.save!
#user_mission.save!
User.first.user_missions[0].mission.title.should == "Created Title"
end
I get:
undefined method `title' for nil:NilClass
When I comment in the line #user_mission.save! it works. The problem is I need this to work in a form and I thought Mongoid automatically saved embedded document's fields.
How do I get the parent User document to save the embedded UserMission's data?
Thanks
UPDATE
Here is the Mission model I forgot to add (wasn't sure if it was important):
class Mission
include Mongoid::Document
belongs_to :user_mission, :inverse_of => :mission
attr_accessible :title
field :title, type: String
validates_presence_of :title
field :lowercase_title
before_create :lower_title_case
field :description, type: String
private
def lower_title_case
self.lowercase_title = self.title.downcase
end
end
AFAIK, embedded documents can't have referenced relations. So the calls to embedded_in and has_one are contradictory.
Other than that, have you tried reordering the lines in your test so that the user gets created first, and then you create user_misison through the user?
user = create(:user)
user_mission = user.user_missions.new
user_mission.mission_title = "Created Title"
user.save!
It looks like what you're trying to do is similar to an SQL JOIN table. If this is what you're trying to achieve, it would be better to take advantage of Mongoid's awesome N-N referenced mapping (unless you need to store extra data in the 'join' collection). I'd do something like:
class User
include Mongoid::Document
has_and_belongs_to_many :missions
end
class Mission
include Mongoid::Document
has_and_belongs_to_many :users
end
If you want to create missions through users, then turn on autosave on the Mission side of the relation.
EDIT: After seeing your Mission model, I realize you're trying to directly reference UserMission (belongs_to :user_mission), an embedded document, directly from your Mission model. Like I said earlier, not doable. You need some relation between the top level docs, User and Mission. You could probably get things to work by changing that line to:
has_many :users
and then changing has_one :mission from the User model to:
belongs_to :mission
You won't be able to use autosave from the User side though.
EDIT: Corrected for proper way to show right way to do 1-n relation from users to missions.
I want to be able to do something like
#foo = MyClass.new
5.times do
#foo.things.build
end
But my #foo needs to have a primary key for this to work, Soo what is the best way to generate primary keys without creating the object?
The purpose for this is to be able to use nested forms more easely
form_builder.fields_for :things do ...
I believe the OP is asking for how to initialize a view action property for use in new action for a standard Rails resource. At this point, there is no ID for the main parent. The solution is simple:
The model:
class ParentObject < ActiveRecord::Base
# the child model in this example is called child_objects
has_many :child_objects, :dependent => :destroy
accepts_nested_attributes_for :child_objects
The controller action for new:
#object = Object.new :example_field => "my field"
#object.child_objects.build :name => "value_1" # pretending that name is a field
#object.child_objects.build :name => "value_2"
Then, in the view:
= form_for(#object) do |f| # top level Object
= f.label :example_field
= f.text_field :example_field
=# the next line loops twice in this example
= f.fields_for :child_objects do |child|
= child.label :name
= child.text_field :name
There is also a good gem called nested_form written by Ryan Bates (https://github.com/ryanb/nested_form) which may help you with the rest of the CRUD operations.
What you probable want is NestedAttributes
Nested attributes allow you to save attributes on associated records through the parent. By default nested attribute updating is turned off, you can enable it using the accepts_nested_attributes_for class method. When you enable nested attributes an attribute writer is defined on the model.
The implementation is different between each ORM, here is for sequel and ActiveRecord
NOTE: Full tutorial also available at Nerdgem
Sequel impementation
Imagine there is a Project class that has many tasks
class Project < Sequel::Model
one_to_many :tasks
end
class Task < Sequel::Model
many_to_one :project
end
To enable the nested attributes you will need include two plugins for the Project class
Sequel::Plugins::NestedAttributes: allows you to create, update, and delete associated objects directly by calling a method on the current object. Nested attributes are defined using the nested_attributes class method:
Sequel::Plugins::InstanceHooks: which is a dependency of NestedAttributes
You can find really good doc on the plugin site
Project.plugin :instance_hooks
Project.plugin :nested_attributes
After that is done you can call the nested_attributes method on the desired class
Project.nested_attributes :tasks
Now you can do this
p = Project.new(:title=>'Project')
p.tasks_attributes = [{:title=>'First Task'}, {:title=>'Second Task'}]
puts p.tasks.inspect
# It will output this
# [#<Task #values={:title=>"First Task"}>, #<Task #values={:title=>"Second Task"}>]
When you save the project it will save both the project and the tasks.
If you can even to edit many tasks at the same.
ActiveRecord implementation
Here is how to use it.
Imagine there is a Project class that has many tasks
Project.rb
class Project < ActiveRecord::Base
attr_accessible :title
has_many :tasks
accepts_nested_attributes_for :tasks
end
Task.rb
class Tasks < ActiveRecord::Base
attr_accessible :title, :project_id
belongs_to :project
end
Now you can do this.
p = Project.new
p.tasks_attributes=[{title: "First Task"}]
p.things
# Would output this
#=> [#<Thing id: nil, title: "First Task", created_at: nil, updated_at: nil, bar_id: nil>]
p.save
When you save the project it will save both the project and the tasks.
If you want to edit many project tasks at the same time you can to this
p.tasks_attributes=[{title: "First Task"},{title: "Second Task"}]
NOTE: there is also a Railscasts that can help you out with nested forms. Orginal Railscast, Revised Railscast
I have created a model with several fields that should accept the same data format (strings, but can be anything, FWIW). I'd like to apply the same validation rule to all those fields. Of course, I can just go ahead and copy/paste stuff, but that would be against DRY principle, and common sense too...
I guess this one is pretty easy, but I'm a Rails newcomer/hipster, so excuse-moi for a trivial question. =)
So if you had say three fields to validate:
:first_name
:last_name
:age
And you wanted them all to be validated? So something like this:
validates_presence_of :first_name, :last_name, :age
Edit: There are numerous different validation methods in Rails )and they're wonderfully flexible). For the format of the field you can use validates_format_of, and then use a Regular Expression to match against it. Here's an example of matching an email:
validates_format_of :email, :with => /^([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
I'd check out the Active Record Validations and Callbacks guide; it provides comprehensive insight about a lot of the features Active Record provides in terms of validation. You can also check out the documentation here.
If you are using any of the built-in validations (presence, length_of) you can apply a single validation to multiple attributes like this:
validates_presence_of :name, :email
If you have custom logic you can create a validator object to house the code and apply it individually
class EmailValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
record.errors[attribute] << (options[:message] || "is not an email") unless
value =~ /^([^#\s]+)#((?:[-a-z0-9]+\.)+[a-z]{2,})$/i
end
end
def Person
validates :home_email, :email => true
validates :work_email, :email => true
end
see: http://thelucid.com/2010/01/08/sexy-validation-in-edge-rails-rails-3/
In Rails 4 you can apply the same validation to multiple columns by using a loop:
[:beds, :baths].each do |column|
validates column, allow_blank: true, length: { maximum: 25 }
end
Both beds and baths are validated using the same validations.
Edit:
In Rails 4.2 you can do this same thing by putting multiple symbols after the validates function call. Example:
validates :beds, :baths, allow_blank: true
Use Themis for this:
# Describe common validation in module
module CommonValidation
extend Themis::Validation
validates_presence_of :foo
validates_length_of :bar, :maximum => 255
end
class ModelA < ActiveRecord::Base
# import validations
include CommonValidation
end
class ModelB < ActiveRecord::Base
# import validations
include CommonValidation
end
Or you can use "with_options", for example:
with_options presence: true do |video|
REQUIRED_COLUMNS.map do |attr|
video.validates attr
end
end
Let's say I have:
class Comment < ActiveRecord::Base
belongs_to :commentable, :polymorphic => true
end
class Article < ActiveRecord::Base
has_many :comments, :as => :commentable
end
class Photo < ActiveRecord::Base
has_many :comments, :as => :commentable
#...
end
now I want to find all comments on Jim's photo:
#jims_photo = Photo.where(:of => "Jim")
#photo_comments = Comment.where(:commentable => #jims_photo)
this seems to not work in rails (Rails 3). The generated query seems to not expand the polymorphic object into commentable_id and commentable_type fields:
SQLite3::SQLException: no such column: comments.commentable:
I'm new to ruby and rails so I might be using the paradigm incorrectly but my expectation was that rails automatically expands
:commentable => #jims_photo
to:
:commentable_id => #jims_photo.id, :commentable_type => #jims_photo.class.name
If you want to be really safe with:
:commentable_id => #jims_photo.id, :commentable_type => #jims_photo.class.name
Then I'd recommend replacing .class.name with .base_class (you don't actually need the name: to_s returns name and will be called automatically).
The reason for this is that when ActiveRecord saves the _type for a polymorphic association it'll use base_class to ensure it's not saving a class which itself is a polymorphic one.
If you play with store_full_sti_class you'll probably have to take even more precautions.
I highly recommend looking at Rails' STI code, here.
The guides for Rails are one of the best so I'd suggest you start reading about Polymorphic Associations
You class declarations looks OK and I'm assuming that you're migrations is as well. But just for the sake of it. Let's say it looks like this:
class CreateComment < ActiveRecord::Migration
def change
create_table :comments do |t|
t.string :name
t.references :commentable, :polymorphic => true
# this is the equivalent of
# t.integer :commentable_id
# t.string :commentable_type
t.timestamps
end
end
end
Not if you have a Article or Photo object and you want to get the comments for that object then Thilo's suggestion is right on. All you need to do is this: #jims_photo.comments
If, on the other hand, you have a an instance of the Comment model, you can get the parent like this: #comment.commentable. But if you want to get Jim's photo comments best to do it like that. Otherwise, you'd have to supply as arguments both the :commentable_id and commentable_type. I'm not aware of any finder that expands the polymorphic object into commentable_id and commentable_type fields for you.
But you can always create a class method for that:
def self.find_by_parent(parent)
where(:commentable_id => parent.id, :commentable_type => parent.class.name)
end