I have a YML file containing fixtures for a Rails model (Comment) which looks like this (pardon the formatting):
comment_a:
id: 1
text: 'foo'
visible: false
comment_b:
id: 2
text: 'bar'
visible: true
comment_c:
id: 3
text: 'baz'
visible: true
I know that I can select an individual Comment fixture like so:
comments(:comment_a)
In one of my acceptance tests, I want to find all the Comments which have visible = true. How do I select a set of Comments that meet certain criteria so I can iterate over them afterwards?
You need made the request by your ActiveRecord Object. Comments.all(:conditions => {:visible => true})
Related
I'm pretty new in Rails, and I got a problem. What is the best way to load data from YML file like this:
projects:
- title: 'family'
todos:
- text: 'Get some milk'
isCompleted: false
- text: 'Cook some bacon'
isCompleted: true
- text: 'Repair the front door'
isCompleted: false
- title: 'work'
todos:
- text: 'Call my boss'
isCompleted: true
- text: 'Finish my work tasks'
isCompleted: true
- text: 'Get fired'
isCompleted: false
- title: 'therest'
todos:
- text: 'Do something'
isCompleted: false
- text: 'Ask a question on stackoverflow'
isCompleted: false
So, I have two models - todo model (text and isCompleted fields) and project model (only title field). Project has_many todos.
I tried to do it like this:
seed_file = Rails.root.join('db', 'seeds', 'seeds.yml')
config = YAML::load_file(seed_file)
Project.create!(config)
But I've got an error:
ActiveModel::UnknownAttributeError: unknown attribute 'projects' for Project.
How can I fix that?
As discussed in chat, you'll need to use something like:
config[:projects].each do |project|
todos = project[:todos]
project.delete(:todos)
new_project = Project.create(project)
todos.each do |todo|
new_project.todos.create(todo)
end
end
That assumes your YML is formatted correctly.
class Project < ApplicationRecord
has_many :todos
accepts_nested_attributes_for :todos
end
Project.create!(title: 'family',
todos_attributes:[{text:'1',isCompleted:false},
{text:'2',isCompleted:false},...])
If you have to use YAML you will have to read the YAML then turn it into something rails can create entities from. Put this in your seeds file:
yaml_hash = YAML.load(File.read('db/your_yaml.yaml'))
rails_arr_of_hashes = yaml_hash['projects'].map{|p| {title: p['title'], todos_attributes: p['todos'] }
Project.create(rails_arr_of_hashes)
put your YAML into db/your_yaml.yaml and then run
rails db:seed
Remember to add the accepts_nested_attributes_for :todos into the Project model.
Good luck learning Rails :)
My app's pricing plan has some features that are unlimited. For example in the "Pro" version you get 10 forms, and in the "Unlimited" version you get unlimited forms.
This is represented in the Plan model by an integer or Float::Infinity.
pro = Plan.find_by name: 'Pro'
pro.forms_limit
#=> 10
unl = Plan.find_by name: 'Unlimited'
unl.forms_limit
#=> Float::INFINITY
In the view:
= t('.forms', count: plan.forms_limit)
I am trying to find an efficient way to interpolate this with I18n. What I'd like to do is:
plans:
index:
forms:
zero: No Forms
one: 1 Form
other: "%{count} Forms"
infinity: Unlimited Forms
This will work, but it results in undesired output like:
"Infinity Forms"
Is there a way to structure this so that Infinity will interpolate "Unlimited" instead of "Infinity"?
Create a file config/locales/plurals.rb with the following
{
en: {
i18n: {
plural: {
keys: [:one, :other, :infinity],
rule: lambda do |n|
if n == 1
:one
elsif n == Float::INFINITY
:infinity
else
:other
end
end
}
}
}
}
and then in my config/locales/en.yml, I have
en:
forms:
zero: 'No Forms'
one: '1 Form'
other: '%{count} Forms'
infinity: 'Unlimited Forms'
added in config/initializers/locale.rb
I18n::Backend::Simple.send(:include, I18n::Backend::Pluralization)
and testing it in IRB
I18n.t(:forms, count: 0)
# => "No Forms"
I18n.t(:forms, count: 1)
# => "1 Form"
I18n.t(:forms, count: 10)
# => "10 Forms"
I18n.t(:forms, count: Float::INFINITY)
# => "Unlimited Forms"
What is this doing?
This isn't nearly as mysterious as I thought when. I first tried it (getting the idea from this related question.
When you include I18n::Backend::Pluralization it's going to start looking for a key i18n.plural.rule that should respond to call, anywhere in the load path. So the plurals.rb file name is unimportant, just need to make sure it's a file in I18n.load_path.
We make that file a ruby file with a hash so that we can set that i18n.plural.rule key to a lambda (so it responds to call) and then the lambda gets called with the count getting passed in. As pointed out there are
I'm trying to find a way to update a single array item using mongoid. I'm using Rails 4 and Mongodb.
My model looks like this
class User
include Mongoid::Document
include Mongoid::Timestamps
field :my_book_list, type: Array, default: []
field :book_name, type: String
I'm able to add entry to the array field using the following code:
User.where(id: self.user_id).add_to_set("my_book_list" => self.book_name)
After I have added data to the array, in the database it looks like this
db.collection.users
{
"_id" : ObjectId("56e09d54a0d00b0528000001"),
"status" : true,
"sign_in_count" : 3,
"my_book_list" :
["Learning Ruby", "MongoDB for Dummies"]
}
What I'm struggling with is to find a Rails / Mongoid way of updating the value of an item in the array by looking for the name.
Simply put, how do I change the value of my_book_list[1] by searching for it through name and not knowing its index. In this case index 1 is "MongoDB for Dummies" and needs to be updated to "MongoDB". So that the "my_book_list" array field looks like this after its updated:
db.collection.users
{
"_id" : ObjectId("56e09d54a0d00b0528000001"),
"status" : true,
"sign_in_count" : 3,
"my_book_list" :
["Learning Ruby", "MongoDB"]
}
How do I achieve this ?
Instead of updating, think of it as adding & removing. You can use pull (https://docs.mongodb.org/ecosystem/tutorial/mongoid-persistence/#atomic)
Where your add to set uniquely adds it to an array, pull removes it based on the name. So assuming this:
user = User.find_by(id: self.user_id)
user.add_to_set(my_book_list: 'First Story')
p user.my_book_list
=> ['First Story']
user.add_to_set(my_book_list: 'Second Story')
p user.my_book_list
=> ['First Story', 'Second Story']
user.add_to_set(my_book_list: 'Third Story')
p user.my_book_list
=> ['First Story', 'Second Story', 'Third Story']
user.pull(my_book_list: 'Second Story')
p user.my_book_list
=> ['First Story', 'Third Story']
If you had duplicates in the set you can use pull_all, but you are using add to set so you won't need to.
This was more of a conceptual problem being a ruby beginner. The answer lies that mongo arrays are simple ruby array and we can use standard ruby methods to update the array.
In this case in my rails console I did this and it worked. The second line finds the array item and replaces with the new wird,
u = User.first
u.my_book_list.map! { |x| x == "MongoDB for Dummies" ? "MongoDB": x}
u.save
Mostly my entries in seeds.rb are simple, like this:
User.create!(
name: "Peter"
admin: false;
# etc.
)
If I get the "Can't mass-assign protected attributes" error, I make a small change in the model, in this case user.rb:
attr_accessible: name, admin
So far so good. But how am I supposed to seed entries into tables generated by a rails gem which adds another engine to my app? Forem, for example. And I'm sure there are others.
I have added these entries to my seeds.rb file:
Forem::Category.create!(
name: "cat1"
)
Forem::Forum.create!(
title: "forum1",
description: "forum1 description",
category_id: 1
)
Forem::Topic.create!(
forum_id: 1,
user_id: 1,
subject: 'topic1',
locked: false,
pinned: false,
hidden: false,
)
Category and Forum are generated, Topic is not:
Can't mass-assign protected attributes: forum_id, user_id, locked, pinned, hidden
If I had a topic.rb model, I would know what to do. But I don't have it. Forem is an engine and I don't know of a way to make some attributes of model topic.rb visible.
I know that this line in application.rb:
config.active_record.whitelist_attributes = true
enables the protection against mass assignment. Disabling it leaves a huge security hole, so it's not an option. And disabling it didn't allow me to seed into topics table anyway.
I've also tried to use fixtures. I added this to my seeds.rb file:
require 'active_record/fixtures'
Fixtures.create_fixtures("#{Rails.root}/test/fixtures", "topics.yml")
test/topics.yml:
one:
id: 1
forum_id: 1
user_id: 1
subject: "topic1"
created_at: 2012-05-19 19:54:19
updated_at: 2012-05-19 19:54:20
locked: false
pinned: false
hidden: false
last_post_at: 2012-05-19 19:54:21
state: "open"
views_count: 3
Error I get is - uninitialized constant Fixtures
What's wrong with my seeds.rb and fixture? Or should I use a migration?
Disabling it leaves a huge security hole, so it's not an option`
Nope, it's not a huge security hole. This is a controversial debate, but attr_accessible (and variants) are (in my and a lot of others opinion) not a good solution to the problem that is preventing users to create/update objects/attributes they should not. Put another way, attr_accessible is a model solution to a controller issue. Because that is the job of the controller to make sure that the data is cleaned and usable, to check wether the current user is allowed to do such things, etc.
So what I'd do would be to remove all references to attr_accessible and set whitelist_attributes to false.
Then it's up to you to filter your params in your controllers. You could do as done in this gist or use rails/strong_parameters, or any other way that might please you.
After that you would no longer have these issues while seeding
Seeds.rb is just ruby code. You don't have to create the whole resource in one line. Try something like this
topic = Forem::Topic.create(
:subject => "topic 1",
:locked => false
# etc
)
topic.user_id = 1
topic.save
I am reading the book Simply Rails by Sitepoint and given these models:
story.rb
class Story < ActiveRecord::Base
validates_presence_of :name, :link
has_many :votes do
def latest
find :all, :order => 'id DESC', :limit => 3
end
end
def to_param
"#{id}-#{name.gsub(/\W/, '-').downcase}"
end
end
vote.rb
class Vote < ActiveRecord::Base
belongs_to :story
end
and given this fixtures
stories.yml
one:
name: MyString
link: MyString
two:
name: MyString2
link: MyString2
votes.yml
one:
story: one
two:
story: one
these tests fail:
story_test.rb
def test_should_have_a_votes_association
assert_equal [votes(:one),votes(:two)], stories(:one).votes
end
def test_should_return_highest_vote_id_first
assert_equal votes(:two), stories(:one).votes.latest.first
end
however, if I reverse the order of the stories, for the first assertion and provide the first vote for the first assertion, it passes
story_test.rb
def test_should_have_a_votes_association
assert_equal [votes(:two),votes(:one)], stories(:one).votes
end
def test_should_return_highest_vote_id_first
assert_equal votes(:one), stories(:one).votes.latest.first
end
I copied everything as it is in the book and have not seen an errata about this. My first conclusion was that the fixture is creating the records from bottom to top as it was declared, but that doesn't make any point
any ideas?
EDIT: I am using Rails 2.9 running in an RVM
Your fixtures aren't getting IDs 1, 2, 3, etc. like you'd expect - when you add fixtures, they get IDs based (I think) on a hash of the table name and the fixture name. To us humans, they just look like random numbers.
Rails does this so you can refer to other fixtures by name easily. For example, the fixtures
#parents.yml
vladimir:
name: Vladimir Ilyich Lenin
#children.yml
joseph:
name: Joseph Vissarionovich Stalin
parent: vladimir
actually show up in your database like
#parents.yml
vladimir:
id: <%= fixture_hash('parents', 'vladimir') %>
name: Vladimir Ilyich Lenin
#children.yml
joseph:
id: <%= fixture_hash('children', 'joseph') %>
name: Joseph Vissarionovich Stalin
parent_id: <%= fixture_hash('parents', 'vladimir') %>
Note in particular the expansion from parent: vladimir to parent_id: <%= ... %> in the child model - this is how Rails handles relations between fixtures.
Moral of the story: Don't count on your fixtures being in any particular order, and don't count on :order => :id giving you meaningful results in tests. Use results.member? objX repeatedly instead of results == [obj1, obj2, ...]. And if you need fixed IDs, hard-code them in yourself.
Hope this helps!
PS: Lenin and Stalin weren't actually related.
Xavier Holt already gave the main answer, but wanted to also mention that it is possible to force rails to read in fixtures in a certain order.
By default rails assigns its own IDs, but you can leverage the YAML omap specification to specify an ordered mapping
# countries.yml
--- !omap
- netherlands:
id: 1
title: Kingdom of Netherlands
- canada:
id: 2
title: Canada
Since you are forcing the order, you have to also specify the ID yourself manually, as shown above.
Also I'm not sure about this part, but I think once you commit to overriding the default rails generated ID and use your own, you have to do the same for all downstream references.
In the above example, suppose each country can have multiple leaders, you would have do something like
# leaders.yml
netherlands-leader:
country_id: 1 #you have to specify this now!
name: Willem-Alexander
You need to manually specify the id that refers to the previous Model (Countries)