I just noticed in my database that inside the Receipts table it is not showing the proper recipient_id for the receiver_id column. I am working between the Questions, Conversations and Receipts table with the Mailboxer gem.
For example here's the issue:
A new entry in the Questions table appears with the
id 552, sender_id 1, and recipient_id 2.
2 new entrys is then made in the Receipts table that's associated with the Questions entry just created (creates 2 entrys by default, one for recipient and other for sender). The details for the
first entry is id 547 and receiver_id 552.
The second entry id is 548 and receiver_id 1.
As you can see for the first entry the receiver_id is being copied from the Questions table id. It should be transferring the Questions table recipient_id instead.
I have no idea at how to fix this.
Questions controller:
def create
#question = Question.new(params[:question])
if #question.save
#message = current_user.send_message(#question, #question.question, "You have a question from #{#question.sender_id}")
redirect_to :back, notice: 'Your question was saved successfully. Thanks!'
else
render :new, alert: 'Sorry. There was a problem saving your question.'
end
end
end
Question model:
acts_as_messageable
attr_accessible :answer, :question, :sender_id, :recipient_id, :conversation_id
belongs_to :user
belongs_to :sender,:class_name => 'User',:foreign_key => 'sender_id'
belongs_to :recipient,:class_name => 'User',:foreign_key => 'recipient_id'
belongs_to :message
belongs_to :conversation
show.html.slim:
center
.message_div
= form_for Question.new, class: 'question_form form-horizontal', role: 'form' do |f|
.form-group
= f.text_field :question, {:placeholder => 'Please add your question...',class:'form-control'}
= f.hidden_field :sender_id, :value => current_user.id
= f.hidden_field :recipient_id, :value => #user.id
= f.submit 'Ask Question', class: 'btn btn-primary'
schema.rb:
create_table "receipts", force: true do |t|
t.integer "receiver_id"
t.string "receiver_type"
t.integer "notification_id", null: false
t.boolean "is_read", default: false
t.boolean "trashed", default: false
t.boolean "deleted", default: false
t.string "mailbox_type", limit: 25
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "receipts", ["notification_id"], name: "index_receipts_on_notification_id", using: :btree
add_foreign_key "receipts", "notifications", name: "receipts_on_notification_id"
Log:
Started POST "/questions" for 127.0.0.1 at 2014-08-08 17:00:12 -0400
Processing by QuestionsController#create as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"nYOcY69oD6WcfNGEJcKGpyupA0CNbYnAJz0SQ+dKtEk=", "question"=>{"question"=>"Stack Overflow can you help me out?", "sender_id"=>"3", "recipient_id"=>"2"}, "commit"=>"Ask Question"}
User Load (0.3ms) SELECT `users`.* FROM `users` WHERE `users`.`auth_token` = 'HkI7Lm4fJYHHf5LHEcMtvA' LIMIT 1
(0.2ms) BEGIN
SQL (0.3ms) INSERT INTO `questions` (`created_at`, `question`, `recipient_id`, `sender_id`, `updated_at`) VALUES ('2014-08-08 21:00:12', 'Stack Overflow can you help me out?', 2, 3, '2014-08-08 21:00:12')
(0.4ms) COMMIT
User Load (0.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 3 ORDER BY `users`.`id` ASC LIMIT 1
(0.1ms) BEGIN
SQL (0.3ms) INSERT INTO `conversations` (`created_at`, `subject`, `updated_at`) VALUES ('2014-08-08 21:00:12', 'You have a question from 3', '2014-08-08 21:00:12')
SQL (0.3ms) INSERT INTO `notifications` (`attachment`, `body`, `conversation_id`, `created_at`, `sender_id`, `sender_type`, `subject`, `type`, `updated_at`) VALUES (NULL, 'Stack Overflow can you help me out?', 419, '2014-08-08 21:00:12', 3, 'User', 'You have a question from 3', 'Message', '2014-08-08 21:00:12')
SQL (0.3ms) INSERT INTO `receipts` (`created_at`, `mailbox_type`, `notification_id`, `receiver_id`, `receiver_type`, `updated_at`) VALUES ('2014-08-08 21:00:12', 'inbox', 459, 577, 'Question', '2014-08-08 21:00:12')
(0.4ms) COMMIT
(0.1ms) BEGIN
SQL (0.4ms) INSERT INTO `receipts` (`created_at`, `is_read`, `mailbox_type`, `notification_id`, `receiver_id`, `receiver_type`, `updated_at`) VALUES ('2014-08-08 21:00:12', 1, 'sentbox', 459, 3, 'User', '2014-08-08 21:00:12')
(0.4ms) COMMIT
(0.1ms) BEGIN
(0.1ms) COMMIT
Redirected to http://localhost:3000/users/user
Completed 302 Found in 562ms (ActiveRecord: 62.3ms)
When you call send_message in your controller, you pass #question as a recipient. It means that the first receipt is indeed built with the question_id as receiver_id. But from what I understand, you want the question's recipient to be sent the message. Simply replace your send_message call with:
current_user.send_message(#question.recipient, #question.question, "You have a question from #{#question.sender_id}")
What's happening behind the scene
The Receipt table from mailboxer can be considered as a proof that a notification has been sent. In your model, by adding
acts_as_messageable
You include the Messageable module described in the mailboxer documentation, including the following send_message method:
def send_message(recipients, msg_body, subject, sanitize_text=true, attachment=nil, message_timestamp = Time.now)
# message = ...
# message building stuff happening
message.deliver false, sanitize_text
end
When you call that method from your controller, it calls the deliver method on this newly created message. Here's the interesting bits of that method, extracted from the mailboxer documentation:
def deliver(reply = false, should_clean = true)
# ... some stuff
#Receiver receipts
temp_receipts = recipients.map { |r| build_receipt(r, 'inbox') }
#Sender receipt
sender_receipt = build_receipt(sender, 'sentbox', true)
temp_receipts << sender_receipt
if temp_receipts.all?(&:save!)
# some other stuff
end
sender_receipt
end
As you can see temp_receipts = recipients.map { |r| build_receipt(r, 'inbox') } builds a receipt for every recipient of the message.
The second interesting line sender_receipt = build_receipt(sender, 'sentbox', true) builds a receipt for the sender. Which in your case is the current_user.
Hope it helps.
Related
I'm trying to save options to DB.
Getting this in DB while saving in console:
Right selected value: "result"=>"1" its 1-4
Right value of updating id: "id"=>"4" its 1-100
I'll appreciate any advice or help with that, trying to figured out for couple hours.
But still getting this error:
CONSOLE
Started POST "/update_result/4" for 65.200.165.210 at 2016-05-03 22:42:16 +0000
Cannot render console from 65.200.165.210! Allowed networks: 127.0.0.1, ::1, 127.0.0.0/127.255.255.255
Processing by DashboardController#update_result as JS
Parameters: {"utf8"=>"✓", "bitbucket"=>{"result"=>"1"}, "id"=>"4"}
Bitbucket Load (0.5ms) SELECT "bitbuckets".* FROM "bitbuckets" WHERE "bitbuckets"."id" = ? LIMIT 1 [["id", 4]]
(0.1ms) begin transaction
(0.2ms) rollback transaction
Completed 500 Internal Server Error in 34ms (ActiveRecord: 0.8ms)
ArgumentError (When assigning attributes, you must pass a hash as an argument.):
app/controllers/dashboard_controller.rb:13:in `update_result'
Rendered /usr/local/rvm/gems/ruby-2.3.0/gems/actionpack-4.2.5/lib/action_dispatch/middleware/templates/rescues/_source.erb (12.0ms)
Rendered /usr/local/rvm/gems/ruby-2.3.0/gems/actionpack-4.2.5/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb (5.9ms)
Rendered /usr/local/rvm/gems/ruby-2.3.0/gems/actionpack-4.2.5/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb (1.7ms)
Rendered /usr/local/rvm/gems/ruby-2.3.0/gems/actionpack-4.2.5/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb (183.9ms)
ROUTES
post 'update_result/:id', to: 'dashboard#update_result', as: :update_result
CONTROLLER
def update_result
#result = Bitbucket.find(params[:id])
#result.update_attributes(params[:result])
end
FORM IN VIEW
<%= form_for(commit, url: update_result_path(commit), :method => :post, :remote => true) do |r| %>
<%= r.select :result, [['Waiting', 1], ['Success', 2], ['Broken - Not Submitted', 3], ['Broken - Quick Win', 4]], { selected: commit.result }, { onclick: "$(this).parent('form').submit();" } %>
<% end %>
DB MIGRATION
create_table :bitbuckets do |t|
t.string :name
t.text :message
t.text :date
t.datetime :remain, :null => false, :default => Time.now
t.integer :assignee
t.integer :result
t.timestamps null: false
end
Solved by adding private function in controller:
CONTROLLER
def update_result
#result = Bitbucket.find(params[:id])
#result.update_attributes(update_params)
end
private
def update_params
params.require(:bitbucket).permit(:result)
end
I retrieve a Post instance using find_by_id, set score on it, and call save. What I see is a really strange error where the SQL is missing the field in WHERE clause. Here's my rails console output:
me> rails c
Loading development environment (Rails 3.2.22.2)
2.1.5 :001 > post = Post.find_by_id 123456
Post Load (0.1ms) SELECT "posts".* FROM "posts" WHERE "posts"."id" = 123456 LIMIT 1
=> #<Post id: 123456, url: "http://www.example.com", title: "Test Title", score: nil>
2.1.5 :002 > post.score = 1
=> 1
2.1.5 :003 > post.save
(0.1ms) begin transaction
(0.2ms) UPDATE "posts" SET "score" = 1 WHERE "posts"."" = 123456
(0.0ms) rollback transaction
ActiveRecord::StatementInvalid: SQLite3::SQLException: no such column: posts.: UPDATE "posts" SET "score" = 1 WHERE "posts"."" = 123456
The problem was that my schema defined posts as
create_table "posts", :id => false, :force => true do |t|
t.integer "id", :limit => 8
...
This means Rails was instructed to not have a primary key column. All I had to do was add
set_primary_key :id
to my model.
I'm trying to build a small expense tracking app using Rails 4.1. When a user submits the expense request, it's state is marked as pending by default. The admin has to approve the request. I'm using state_machine gem to do this.
Comments are added from the expense show page using a nested_form_for like this:
<%= nested_form_for (#expense) do |f| %>
<div class="form-group">
<%= f.label :state %><br />
<%= f.collection_select :state, #expense.state_transitions, :event, :human_to_name, :include_blank => #expense.human_state_name, class: "form-control" %>
</div>
<%= f.fields_for :comments, #expense.comments.build do |comment| %>
<div class="form-group">
<%= comment.label :comment%>
<%= comment.text_area :comment, class: "form-control" %>
</div>
<% end %>
<%= f.submit "Submit", class: "btn btn-primary" %>
<% end %>
The controller looks like:
class ExpensesController < ApplicationController
def new
#expense = Expense.new
#item = #expense.items.build
#comment = #expense.comments.build
end
def show
#expense = Expense.find(params[:id])
#items = Item.where(:expense_id => #expense.id)
end
def update
#expense = Expense.find(params[:id])
if #expense.update(expense_params)
if #expense.state == "approved"
ExpenseMailer.expense_approved(#expense).deliver
flash[:notice] = "Expense Report Updated"
redirect_to expenses_path
elsif #expense.state = "rejected"
ExpenseMailer.expense_declined(#expense).deliver
flash[:notice] = "Expense Report Updated"
redirect_to expenses_path
end
else
render 'edit'
end
end
private
def expense_params
params.require(:expense).permit(:claim, :department_id, :expense_type_id, :expense_attachment, :state, :notes, items_attributes: [:id, :description, :amount, :issue_date, :_destroy], comments_attributes:[:id, :comment, :expense_id])
end
The problem is, if I add a comment without changing the state from the dropdown, I get the 'state is invalid' error and the edit page is shown. I can get past this by simply hitting the update button. But, the comments aren't created. On the other hand, if I change the state and add a comment, comments are shown without any issue.
The params value for that is:
Started PATCH "/expenses/14" for 127.0.0.1 at 2014-08-15 13:31:40 +0530
Processing by ExpensesController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"MAEL2UYzos76NV6/eumHkXcpR2ge09wm6eOGQ+eEGCA=", "expense"=>{"state"=>"", "comments_attributes"=>{"0"=>{"comment"=>"vv"}}}, "commit"=>"Submit", "id"=>"14"}
The expense model with state machine looks like:
state_machine initial: :pending do
state :pending
state :approved
state :rejected
event :approved do
transition [:pending, :rejected] => :approved
end
event :rejected do
transition [:pending, :approved] => :rejected
end
end
Guess I'm making some mistake when it comes to building the comment attributes. Can someone let me know where I have to make changes?
Logger info for rejection:
Started GET "/expenses/17" for 127.0.0.1 at 2014-08-15 16:22:43 +0530
Processing by ExpensesController#show as HTML
Parameters: {"id"=>"17"}
[1m[35mExpense Load (0.2ms)[0m SELECT "expenses".* FROM "expenses" WHERE "expenses"."id" = ? LIMIT 1 [["id", 17]]
[1m[36mItem Load (0.1ms)[0m [1mSELECT "items".* FROM "items" WHERE "items"."expense_id" = ?[0m [["expense_id", 17]]
[1m[35mComment Load (0.2ms)[0m SELECT "comments".* FROM "comments" WHERE "comments"."expense_id" = ? [["expense_id", 17]]
Rendered expenses/show.html.erb within layouts/application (16.2ms)
Completed 200 OK in 45ms (Views: 42.8ms | ActiveRecord: 0.5ms)
Started PATCH "/expenses/17" for 127.0.0.1 at 2014-08-15 16:22:53 +0530
Processing by ExpensesController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"MAEL2UYzos76NV6/eumHkXcpR2ge09wm6eOGQ+eEGCA=", "expense"=>{"state"=>"rejected", "comments_attributes"=>{"0"=>{"comment"=>"checking logger for rejected!"}}}, "commit"=>"Submit", "id"=>"17"}
[1m[36mExpense Load (0.2ms)[0m [1mSELECT "expenses".* FROM "expenses" WHERE "expenses"."id" = ? LIMIT 1[0m [["id", 17]]
[1m[35m (0.1ms)[0m begin transaction
[1m[36mSQL (8.1ms)[0m [1mUPDATE "expenses" SET "state" = ?, "updated_at" = ? WHERE "expenses"."id" = 17[0m [["state", "rejected"], ["updated_at", "2014-08-15 10:52:53.030676"]]
[1m[35mSQL (0.2ms)[0m INSERT INTO "comments" ("comment", "created_at", "expense_id", "updated_at") VALUES (?, ?, ?, ?) [["comment", "checking logger for rejected!"], ["created_at", "2014-08-15 10:52:53.040889"], ["expense_id", 17], ["updated_at", "2014-08-15 10:52:53.040889"]]
[1m[36m (4.2ms)[0m [1mcommit transaction[0m
Redirected to http://localhost:3000/expenses
Completed 302 Found in 24ms (ActiveRecord: 12.8ms)
Logger info for approval:
Started GET "/expenses/16" for 127.0.0.1 at 2014-08-15 16:22:30 +0530
Processing by ExpensesController#show as HTML
Parameters: {"id"=>"16"}
[1m[35mExpense Load (0.3ms)[0m SELECT "expenses".* FROM "expenses" WHERE "expenses"."id" = ? LIMIT 1 [["id", 16]]
[1m[36mItem Load (0.2ms)[0m [1mSELECT "items".* FROM "items" WHERE "items"."expense_id" = ?[0m [["expense_id", 16]]
[1m[35mComment Load (0.3ms)[0m SELECT "comments".* FROM "comments" WHERE "comments"."expense_id" = ? [["expense_id", 16]]
Rendered expenses/show.html.erb within layouts/application (167.3ms)
Completed 200 OK in 244ms (Views: 213.7ms | ActiveRecord: 1.1ms)
Started PATCH "/expenses/16" for 127.0.0.1 at 2014-08-15 16:22:41 +0530
Processing by ExpensesController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"MAEL2UYzos76NV6/eumHkXcpR2ge09wm6eOGQ+eEGCA=", "expense"=>{"state"=>"approved", "comments_attributes"=>{"0"=>{"comment"=>"checking logger!"}}}, "commit"=>"Submit", "id"=>"16"}
[1m[36mExpense Load (0.2ms)[0m [1mSELECT "expenses".* FROM "expenses" WHERE "expenses"."id" = ? LIMIT 1[0m [["id", 16]]
[1m[35m (0.1ms)[0m begin transaction
[1m[36mSQL (0.5ms)[0m [1mUPDATE "expenses" SET "state" = ?, "updated_at" = ? WHERE "expenses"."id" = 16[0m [["state", "approved"], ["updated_at", "2014-08-15 10:52:41.604580"]]
[1m[35mSQL (0.5ms)[0m INSERT INTO "comments" ("comment", "created_at", "expense_id", "updated_at") VALUES (?, ?, ?, ?) [["comment", "checking logger!"], ["created_at", "2014-08-15 10:52:41.607555"], ["expense_id", 16], ["updated_at", "2014-08-15 10:52:41.607555"]]
[1m[36m (4.0ms)[0m [1mcommit transaction[0m
Redirected to http://localhost:3000/expenses
Completed 302 Found in 17ms (ActiveRecord: 5.3ms)
I don't know why you're getting an error, but I'll give you some ideas for the state_machine / aasm gems
--
State Machines
Since Rails is object-orientated, you have to appreciate how these state machine gems work - they are an extrapolation of the electronics method of setting up a "state machine" (to predicate finite "states" within a circuit):
What I'm trying to demonstrate with this is that by including a state machine in your application, you're actually indicating the state of an object (it's not just another attribute)
Currently, you're treating the state attribute of your Comment model as an attribute, when it can be treated as an object in itself
--
Object
Notice this functionality from the State Machine repo:
Notice how that has nothing to do with the state attribute?
I think you'd be better treating the state method as what it is - a way to influence the state_machine itself. I would do that in several ways:
Set the state "default" state in the state_machine declaration
Validate the state object using OOP principles
#app/models/expense.rb
Class Expense < ActiveRecord::Base
state_machine :state, :initial => :pending do #-> sets the state to "pending" unless specified otherwise
end
end
#app/controllers/expenses_controller.rb
Class ExpensesController < ApplicationController
def update
if #expense.approved?
...
end
end
end
--
Fix
In regards to your being unable to create a comment, I think the problem will be two-fold
Firstly, you're building your comments within the view. Apart from anything, this is bad practice (against MVC) - you'll be best building the associated objects inside your model:
#app/models/expense.rb
Class Expense < ActiveRecord::Base
def self.build id
expense = (id.present?) self.find id : self.new
expense.comments.build
return expense
end
end
This allows you to perform the following:
#app/controllers/expenses_controller.rb
Class ExpensesController < ApplicationController
def new
#expense = Expense.build
end
def edit
#expense = Expense.build params[:id]
end
end
This will basically give your nested comments form the pre-built nested objects required to fire the form for the edit & new methods (so you don't need to call #expense.comments.build in your view)
In regards to the non-saving functionality - I would certainly look at how you're saving the state attribute. I suspect it will be down to you not passing the attribute correctly (IE that you're using an incorrect value for the state param upon default submission)
I would recommend using the following:
Investigate your params from the "default" update
Does the state attribute match your model definition of the attributes?
If it does not, that will be your problem
--
UPDATE
Thanks for the update
Okay, so the problem seems to be that the state value is not being passed if it's default. I think the way to fix this will be to set a default value for the collection_select:
Remove :include_blank => #expense.human_state_name
Replace with <%= f.collection_select :state, #expense.state_transitions, :event, :human_to_name, { selected: #expense.human_state_name}, class: "form-control" %>
Update 2
Since state_machine gives you the ability to track & fire instance methods after a successful transition, you may wish to do the following:
#app/models/expense.rb
Class Expense < ActiveRecord::Base
state_machine :state, :initial => :pending do
state :pending
state :approved
state :rejected
event :approve do
transition [:pending, :rejected] => :approved
end
event :reject do
transition [:pending, :approved] => :rejected
end
after_transition :on => :approved, :do => :send_approval_email
after_transition :on => :rejected, :do => :send_rejection_email
def send_approval_email
ExpenseMailer.expense_approved(self).deliver #-> might need to call outide of state_machine block
end
def send_rejection_email
ExpenseMailer.expense_declined(self).deliver
end
end
end
This will give you the ability to perform the following:
#app/controllers/expenses_controller.rb
Class ExpensesController < ApplicationController
def update
#expense = Expense.find params[:id]
if #expense.update(expense_params)
flash[:notice] = "Expense Report Updated"
redirect_to expenses_path
end
end
end
By the way, you need to change your "events" to have different names to your "states". As per my object-oriented references above, you need to be able to call the likes of #object.approve etc
I'm building my first Rails app and one of the important features in it is having users who speak and/or want to learn languages. In the user edit profile page, I allow him/her to choose what languages he/she speaks and/or wants to learn from a list (I'm using ryanb's nested_form gem):
There are 3 models involved in this: User, Speaks, Language
The languages table is just a table with languages of the world, it doesn't change. It consists basically of ISO codes for languages and their names. I populate it by running a script that reads from the official file I downloaded. Still I was simply using Rails defaults, so the table had an id column, and it was all working fine.
Then I decided to make a change and remove the id column, because it didn't make any sense anyway. I want my app to be up to date with the ISO list. I want the ISO code to identify the languages, not a meaningless id. I want to use
user.speaks.create!(language_id: "pt", level: 6)
instead of
user.speaks.create!(language_id: 129, level: 6)
I know it's unlikely that the ISO list will change but, if it does, I want to simply run my script again with the new file and not worry if the id column will still match the same ISO code as before. So I made the change. Now I can use user.speaks.create the way I want and the association works perfectly in the console. The problem is my form simply isn't working anymore. The data is sent but I don't understand the logs. They show a bunch of SELECTS but no INSERTS or UPDATES, I don't get why. Does anybody have any idea?
Here are my models:
class User < ActiveRecord::Base
attr_accessible ..., :speaks, :speaks_attributes, :wants_to_learn_attributes
has_many :speaks, :class_name => "Speaks", :dependent => :destroy
has_many :speaks_languages, :through => :speaks, :source => :language #, :primary_key => "iso_639_1_code"
has_many :wants_to_learn, :class_name => "WantsToLearn", :dependent => :destroy
has_many :wants_to_learn_languages, :through => :wants_to_learn, :source => :language #, :primary_key => "iso_639_1_code"
...
accepts_nested_attributes_for :speaks #, :reject_if => :speaks_duplicate, :allow_destroy => true
accepts_nested_attributes_for :wants_to_learn #, :reject_if => :wants_to_learn_duplicate, :allow_destroy => true
# EDIT 1: I remembered these pieces of code silenced errors, so I commented them out
...
end
class Speaks < ActiveRecord::Base
self.table_name = "speak"
attr_accessible :language, :language_id, :level
belongs_to :user
belongs_to :language
validates :user, :language, :level, presence: true
...
end
#EDIT 4:
class WantsToLearn < ActiveRecord::Base
self.table_name = "want_to_learn"
attr_accessible :language, :language_id
belongs_to :user
belongs_to :language
validates :user, :language, presence: true
...
end
class Language < ActiveRecord::Base
attr_accessible :iso_639_1_code, :name_en, :name_fr, :name_pt
has_many :speak, :class_name => "Speaks"
has_many :users_who_speak, :through => :speak, :source => :user
has_many :want_to_learn, :class_name => "WantsToLearn"
has_many :users_who_want_to_learn, :through => :want_to_learn, :source => :user
end
Controller:
def update
logger.debug params
if #user.update_attributes(params[:user])
#user.save
flash[:success] = "Profile updated"
sign_in #user
redirect_to :action => :edit
else
render :action => :edit
end
end
View:
<%= nested_form_for(#user, :html => { :class => "edit-profile-form"} ) do |f| %>
<%= render 'shared/error_messages' %>
<table border="0">
<tr><td colspan="2"><h2 id="languages" class="bblabla">Languages</h2></td></tr>
<tr>
<td><span>Languages you speak</span></td>
<td class="languages-cell">
<div id="speaks">
<%= f.fields_for :speaks, :wrapper => false do |speaks| %>
<div class="fields">
<%= speaks.select(:language_id,
Language.all.collect {|lang| [lang.name_en, lang.id]},
{ :selected => speaks.object.language_id, :include_blank => false },
:class => 'language') %>
<%= speaks.label :level, "Level: " %>
<%= speaks.select(:level, Speaks.level_options, { :selected => speaks.object.level }, :class => 'level') %>
<%= speaks.link_to_remove raw("<i class='icon-remove icon-2x'></i>"), :class => "remove-language" %>
</div>
<% end %>
</div>
<p class="add-language"><%= f.link_to_add "Add language", :speaks, :data => { :target => "#speaks" } %></p>
</td>
</tr>
...
Log:
Started PUT "/users/1" for 127.0.0.1 at 2013-07-19 08:41:16 -0300
Processing by UsersController#update as HTML
Parameters: {"utf8"=>"✓", "authenticity_token"=>"ZmaU9...", "user"=>{"speaks_attributes"=>{"0"=>{"language_id"=>"pt", "level"=>"6", "_destroy"=>"false"}, "1374234067848"=>{"language_id"=>"en", "level"=>"5", "_destroy"=>"false"}}, "wants_to_learn_attributes"=>{"0"=>{"language_id"=>"ro", "_destroy"=>"false", "id"=>"1"}}, "home_location_attributes"=>{"google_id"=>"7789d9...", "latitude"=>"-22.9035393", "longitude"=>"-43.20958689999998", "city"=>"Rio de Janeiro", "neighborhood"=>"", "administrative_area_level_1"=>"Rio de Janeiro", "administrative_area_level_2"=>"", "country_id"=>"BR", "id"=>"1"}, "gender"=>"2", "relationship_status"=>"2", "about_me"=>""}, "commit"=>"Save changes", "id"=>"1"}
[1m[35mUser Load (0.3ms)[0m SELECT "users".* FROM "users" WHERE "users"."remember_token" = 'bjdvI...' LIMIT 1
[1m[36mUser Load (0.2ms)[0m [1mSELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1[0m [["id", "1"]]
{"utf8"=>"✓", "_method"=>"put", "authenticity_token"=>"ZmaU9W...", "user"=>{"speaks_attributes"=>{"0"=>{"language_id"=>"pt", "level"=>"6", "_destroy"=>"false"}, "1374234067848"=>{"language_id"=>"en", "level"=>"5", "_destroy"=>"false"}}, "wants_to_learn_attributes"=>{"0"=>{"language_id"=>"ro", "_destroy"=>"false", "id"=>"1"}}, "home_location_attributes"=>{"google_id"=>"7789d9...", "latitude"=>"-22.9035393", "longitude"=>"-43.20958689999998", "city"=>"Rio de Janeiro", "neighborhood"=>"", "administrative_area_level_1"=>"Rio de Janeiro", "administrative_area_level_2"=>"", "country_id"=>"BR", "id"=>"1"}, "gender"=>"2", "relationship_status"=>"2", "about_me"=>""}, "commit"=>"Save changes", "action"=>"update", "controller"=>"users", "id"=>"1"}
[1m[35m (0.1ms)[0m BEGIN
[1m[36mWantsToLearn Load (0.2ms)[0m [1mSELECT "want_to_learn".* FROM "want_to_learn" WHERE "want_to_learn"."user_id" = 1 AND "want_to_learn"."id" IN (1)[0m
[1m[35mLocation Load (0.3ms)[0m SELECT "locations".* FROM "locations" WHERE "locations"."google_id" = '7789d...' AND "locations"."latitude" = '-22.9035393' AND "locations"."longitude" = '-43.20958689999998' AND "locations"."city" = 'Rio de Janeiro' AND "locations"."neighborhood" = '' AND "locations"."administrative_area_level_1" = 'Rio de Janeiro' AND "locations"."administrative_area_level_2" = '' AND "locations"."country_id" = 'BR' LIMIT 1
[1m[36mUser Exists (40.0ms)[0m [1mSELECT 1 AS one FROM "users" WHERE (LOWER("users"."email") = LOWER('ariel#pontes.com') AND "users"."id" != 1) LIMIT 1[0m
[1m[35m (96.7ms)[0m UPDATE "users" SET "remember_token" = 'd0pb...', "updated_at" = '2013-07-19 11:41:16.808422' WHERE "users"."id" = 1
[1m[36m (28.7ms)[0m [1mCOMMIT[0m
[1m[35m (0.1ms)[0m BEGIN
[1m[36mUser Exists (0.3ms)[0m [1mSELECT 1 AS one FROM "users" WHERE (LOWER("users"."email") = LOWER('ariel#pontes.com') AND "users"."id" != 1) LIMIT 1[0m
[1m[35m (0.3ms)[0m UPDATE "users" SET "remember_token" = 'gKlW...', "updated_at" = '2013-07-19 11:41:17.072654' WHERE "users"."id" = 1
[1m[36m (0.4ms)[0m [1mCOMMIT[0m
Rendered shared/_error_messages.html.erb (0.0ms)
[1m[35mSpeaks Load (0.3ms)[0m SELECT "speak".* FROM "speak" WHERE "speak"."user_id" = 1
[1m[36mWantsToLearn Load (0.2ms)[0m [1mSELECT "want_to_learn".* FROM "want_to_learn" WHERE "want_to_learn"."user_id" = 1[0m
[1m[35mLanguage Load (0.3ms)[0m SELECT "languages".* FROM "languages"
[1m[36mCountry Load (0.3ms)[0m [1mSELECT "countries".* FROM "countries" WHERE "countries"."iso_3166_code" = 'BR' LIMIT 1[0m
[1m[35mCACHE (0.0ms)[0m SELECT "languages".* FROM "languages"
[1m[36mCACHE (0.0ms)[0m [1mSELECT "languages".* FROM "languages" [0m
Rendered users/edit.html.erb within layouts/application (39.8ms)
Rendered layouts/_shim.html.erb (0.0ms)
Rendered layouts/_header.html.erb (1.1ms)
Rendered layouts/_footer.html.erb (0.2ms)
Completed 200 OK in 576ms (Views: 160.7ms | ActiveRecord: 168.7ms)
Hope someone has an insight cause I've been looking all over the internet for the past 2 days with no luck. Thanks in advance!
EDIT 1
I placed the accepts_nested_attributes_for lines after the associations were made, as suggested by ovatsug25, but it didn't seem to make any change. I remembered, however, that there were some options in the User model that silenced errors, which of course hinders the debugging, so I commented these options out. Now I have the following error:
PG::Error: ERROR: operator does not exist: character varying = integer
LINE 1: ...M "languages" WHERE "languages"."iso_639_1_code" = 0 LIMIT ...
^
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT "languages".* FROM "languages" WHERE "languages"."iso_639_1_code" = 0 LIMIT 1
I have NO IDEA why Rails is trying to select a language with pk = 0. Even if the pk WAS an integer this wouldn't make sense (would it???) since the default id starts from 1. And even if it started from zero, why would it be trying to select it anyway? Where is this zero comming from?? And I can't "add explicit type casts". The pk is a string and will never be 0 or '0' for that matter. This query doesn't make sense and simply isn't supposed to happen!
EDIT 2
I tried to update the attributes in the console and got the following:
irb(main):006:0> ariel = User.find(1)
User Load (101.5ms) SELECT "users".* FROM "users" WHERE "users"."id" = $1 LIMIT 1 [["id", 1]]
=> #<User id: 1, first_name: "Ariel", last_name: "Pontes", ...>
irb(main):007:0> params = {"user"=>{"speaks_attributes"=>{"0"=>{"language_id"=>"pt", "level"=>"6", "_destroy"=>"false"}, "1374444891951"=>{"language_id"=>"en", "l
evel"=>"5", "_destroy"=>"false"}}, "wants_to_learn_attributes"=>{"0"=>{"language_id"=>"ro", "_destroy"=>"false"}}, "home_location_attributes"=>{"google_id"=>"778...c5a", "latitude"=>"-22.9035393", "longitude"=>"-43.20958689999998", "city"=>"Rio de Janeiro", "neighborhood"=>"", "administrative
_area_level_1"=>"Rio de Janeiro", "administrative_area_level_2"=>"", "country_id"=>"BR", "id"=>"1"}, "gender"=>"2", "relationship_status"=>"2", "about_me"=>""}}
=> {"user"=>{"speaks_attributes"=>{"0"=>{"language_id"=>"pt", "level"=>"6", "_destroy"=>"false"}, "1374444891951"=>{"language_id"=>"en", "level"=>"5", "_destroy"=
>"false"}}, "wants_to_learn_attributes"=>{"0"=>{"language_id"=>"ro", "_destroy"=>"false"}}, "home_location_attributes"=>{"google_id"=>"778...c5a", "latitude"=>"-22.9035393", "longitude"=>"-43.20958689999998", "city"=>"Rio de Janeiro", "neighborhood"=>"", "administrative_area_level_1"=>"Rio de
Janeiro", "administrative_area_level_2"=>"", "country_id"=>"BR", "id"=>"1"}, "gender"=>"2", "relationship_status"=>"2", "about_me"=>""}}
irb(main):008:0> ariel.update_attributes(params[:user])
(0.1ms) BEGIN
User Exists (0.5ms) SELECT 1 AS one FROM "users" WHERE (LOWER("users"."email") = LOWER('ariel#pontes.com') AND "users"."id" != 1) LIMIT 1
(24.9ms) UPDATE "users" SET "remember_token" = '0tv...Cw', "updated_at" = '2013-07-22 15:45:30.705217' WHERE "users"."id" = 1
(54.3ms) COMMIT
=> true
irb(main):009:0>
Basically, it only updates the remember_token and updated_at for some reason.
EDIT 3
I tried to update only the spoken languages and it worked:
irb(main):012:0> ariel.update_attributes({"speaks_attributes"=>{"0"=>{"language_id"=>"pt", "level"=>"6", "_destroy"=>"false"}, "1374444891951"=>{"language_id"=>"e
n", "level"=>"5", "_destroy"=>"false"}}})
(0.2ms) BEGIN
User Load (0.4ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
Language Load (0.8ms) SELECT "languages".* FROM "languages" WHERE "languages"."iso_639_1_code" = 'pt' LIMIT 1
User Load (0.3ms) SELECT "users".* FROM "users" WHERE "users"."id" = 1 LIMIT 1
Language Load (0.2ms) SELECT "languages".* FROM "languages" WHERE "languages"."iso_639_1_code" = 'en' LIMIT 1
User Exists (0.2ms) SELECT 1 AS one FROM "users" WHERE (LOWER("users"."email") = LOWER('ariel#pontes.com') AND "users"."id" != 1) LIMIT 1
(0.2ms) UPDATE "users" SET "remember_token" = 'MYh5X1XoF6OsVIo3rhDNzQ', "updated_at" = '2013-07-22 22:05:08.198025' WHERE "users"."id" = 1
SQL (42.9ms) INSERT INTO "speak" ("created_at", "language_id", "level", "updated_at", "user_id") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["created_at", Mo
n, 22 Jul 2013 22:05:08 UTC +00:00], ["language_id", "pt"], ["level", 6], ["updated_at", Mon, 22 Jul 2013 22:05:08 UTC +00:00], ["user_id", 1]]
SQL (0.4ms) INSERT INTO "speak" ("created_at", "language_id", "level", "updated_at", "user_id") VALUES ($1, $2, $3, $4, $5) RETURNING "id" [["created_at", Mon
, 22 Jul 2013 22:05:08 UTC +00:00], ["language_id", "en"], ["level", 5], ["updated_at", Mon, 22 Jul 2013 22:05:08 UTC +00:00], ["user_id", 1]]
(14.7ms) COMMIT
=> true
I'm starting to fear it may be a case of witchcraft.
PS: Does anybody know why it loads the User 3 times? Seems rather pointless and wasteful.
The biggest clue is this error that caught your eye:
HINT: No operator matches the given name and argument type(s). You might need to add explicit type casts.
: SELECT "languages".* FROM "languages" WHERE "languages"."iso_639_1_code" = 0 LIMIT 1
If you're providing a string value for a model attribute, but the underlying database column is a numeric column, Rails will try to convert the string value to the appropriate numeric type. So, if the underlying column is of type integer, the string input will be interpreted as an integer, using String#to_i. If the string doesn't start with a number, it will be converted to 0.
The Rails console (rails c) can be a useful tool for debugging issues like this. In this case, on the console, you can run WantsToLearn.columns_hash['language_id'].type to see what type Rails thinks it should be using for that attribute. Of course, you can also just as easily check the migrations.
I used to have a problem like this and solved it by segreating the accepts_attributes_for calls to the very bottom after all associations and accessible attributes have been declared. (I also merged attr_accesible into one call. I think ryanb says something in this video about the order of the calls. http://railscasts.com/episodes/196-nested-model-form-revised?view=asciicast.
Makes sense? No. But it worked for me.
I'm a Rails beginner and to learn it I'm building a simple time tracking app. I want to populate an administrator's dashboard with a ton of information from many tables with nested information.
What would be the best practice for querying the database to request all of the data for one company to view a dashboard of all clients, projects, tasks, adjustments and minutes?
Here's how the data is structured:
Company
has_many clients
Client
belongs_to company
has_many projects
Project
belongs_to client
has_many tasks
Task
belongs_to project
has_many minutes
Minute
belongs_to task
This data structure might be really bad. I don't know.
An example view of the data:
Activision
-- Website Redesign
--- Development
---- 100 Minutes
I'm starting with this but I'm pretty but it could be totally backwards (Users belong to Companies):
#clients = Client.find_all_by_company_id(current_user.company_id)
#clients.each do |client|
project = Project.find_all_by_client_id(client.id)
puts project.name
project.each do |project|
task = Task.find_all_by_project_id(project.id)
puts task.name
end
end
I guess the question can also be asked: Is there a good book or resource that fully describes Rails ActiveRecord best practices?
Use the includes method to eagerly load the associations.
Example from the guides
Category.includes(:posts => [{:comments => :guest}, :tags]).find(1)
Based on what you said, that should be:
require 'active_record'
require 'logger'
# ===== Config =====
ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: ':memory:'
ActiveRecord::Base.logger = Logger.new $stdout
ActiveSupport::LogSubscriber.colorize_logging = false
# ===== Schema =====
ActiveRecord::Schema.define do
self.verbose = false
create_table :clients do |t|
t.string :name
t.integer :company_id
end
create_table :companies do |t|
t.string :name
end
create_table :projects do |t|
t.string :name
t.integer :client_id
end
create_table :tasks do |t|
t.string :name
t.integer :project_id
end
create_table :minutes do |t|
t.integer :quantity
t.integer :task_id
end
end
# ===== Classes =====
class Company < ActiveRecord::Base
has_many :clients
end
class Client < ActiveRecord::Base
belongs_to :company
has_many :projects
end
class Project < ActiveRecord::Base
belongs_to :client
has_many :tasks
end
class Task < ActiveRecord::Base
belongs_to :project
has_many :minutes
end
class Minute < ActiveRecord::Base
belongs_to :task
end
# ===== Data =====
Company.create! name: 'Activision' do |company|
company.clients.build name: 'Robert Kotick' do |client|
client.projects.build name: 'Website Redesign' do |project|
project.tasks.build name: 'Development' do |task|
task.minutes.build quantity: 100
end
end
end
end
# ===== Querying and displaying =====
company = Company.find_by_name 'Activision'
clients = Client.includes(projects: {tasks: :minutes}).where(company_id: company.id)
print "\n----- The query makes four requests, regardless of how much data you have. -----\n\n"
clients.inspect # do this to force loading since AR queries are lazy
print "\n----- some representation of the data (notice no queries while iterating through) -----\n\n"
clients.each do |client|
puts client.name
client.projects.each do |project|
puts "-- #{project.name}"
project.tasks.each do |task|
puts "--- #{task.name}"
task.minutes.each do |minute|
puts "---- #{minute.quantity}"
end
end
end
end
# ===== Output =====
# >> D, [2012-09-12T00:01:42.755414 #72855] DEBUG -- : (0.7ms) select sqlite_version(*)
# >> D, [2012-09-12T00:01:42.755890 #72855] DEBUG -- : (0.2ms) CREATE TABLE "clients" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "company_id" integer)
# >> D, [2012-09-12T00:01:42.756327 #72855] DEBUG -- : (0.1ms) CREATE TABLE "companies" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255))
# >> D, [2012-09-12T00:01:42.756728 #72855] DEBUG -- : (0.1ms) CREATE TABLE "projects" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "client_id" integer)
# >> D, [2012-09-12T00:01:42.757122 #72855] DEBUG -- : (0.1ms) CREATE TABLE "tasks" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "name" varchar(255), "project_id" integer)
# >> D, [2012-09-12T00:01:42.757531 #72855] DEBUG -- : (0.1ms) CREATE TABLE "minutes" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, "quantity" integer, "task_id" integer)
# >> D, [2012-09-12T00:01:42.906877 #72855] DEBUG -- : (0.0ms) begin transaction
# >> D, [2012-09-12T00:01:42.909242 #72855] DEBUG -- : SQL (0.5ms) INSERT INTO "companies" ("name") VALUES (?) [["name", "Activision"]]
# >> D, [2012-09-12T00:01:42.934937 #72855] DEBUG -- : SQL (24.7ms) INSERT INTO "clients" ("company_id", "name") VALUES (?, ?) [["company_id", 1], ["name", "Robert Kotick"]]
# >> D, [2012-09-12T00:01:42.936110 #72855] DEBUG -- : SQL (0.1ms) INSERT INTO "projects" ("client_id", "name") VALUES (?, ?) [["client_id", 1], ["name", "Website Redesign"]]
# >> D, [2012-09-12T00:01:42.937001 #72855] DEBUG -- : SQL (0.1ms) INSERT INTO "tasks" ("name", "project_id") VALUES (?, ?) [["name", "Development"], ["project_id", 1]]
# >> D, [2012-09-12T00:01:42.937767 #72855] DEBUG -- : SQL (0.1ms) INSERT INTO "minutes" ("quantity", "task_id") VALUES (?, ?) [["quantity", 100], ["task_id", 1]]
# >> D, [2012-09-12T00:01:42.938005 #72855] DEBUG -- : (0.0ms) commit transaction
# >> D, [2012-09-12T00:01:42.939882 #72855] DEBUG -- : Company Load (0.1ms) SELECT "companies".* FROM "companies" WHERE "companies"."name" = 'Activision' LIMIT 1
# >>
# >> ----- The query makes four requests, regardless of how much data you have. -----
# >>
# >> D, [2012-09-12T00:01:42.940458 #72855] DEBUG -- : Client Load (0.1ms) SELECT "clients".* FROM "clients" WHERE "clients"."company_id" = 1
# >> D, [2012-09-12T00:01:42.943272 #72855] DEBUG -- : Project Load (0.1ms) SELECT "projects".* FROM "projects" WHERE "projects"."client_id" IN (1)
# >> D, [2012-09-12T00:01:42.943919 #72855] DEBUG -- : Task Load (0.1ms) SELECT "tasks".* FROM "tasks" WHERE "tasks"."project_id" IN (1)
# >> D, [2012-09-12T00:01:42.944520 #72855] DEBUG -- : Minute Load (0.1ms) SELECT "minutes".* FROM "minutes" WHERE "minutes"."task_id" IN (1)
# >>
# >> ----- some representation of the data (notice no queries while iterating through) -----
# >>
# >> Robert Kotick
# >> -- Website Redesign
# >> --- Development
# >> ---- 100
This is a horrible Law of Demeter violation, if any of these things change at any point, whether in their structure or naming, we will have to come fix this code. I'm not really sure how to deal with that without introducing lots of abstractions.
Regarding a book, there have been many, but I honestly don't think the Rails world has figured out yet what constitute best ActiveRecord practices (in fact, there's a large portion of the community that thinks almost all ActiveRecord practices are just terrible -- I'm mostly in that camp).
But if you want things like the above, which says to use #includes to eager load associations, then the guides are a great place to find out information like that. I also really enjoyed this blog and videos.
This produces the same you have
#clients = current_user.company.clients
#clients.each do |client|
projects = client.projects
# puts project.name # makes no sense here
projects.each do |project|
project.tasks.each do |task|
puts task.name
end
end
end
try something like
Client.includes(
:company =>{:projects=>:tasks})
all of the above should be connected (via has_one, has_many, belongs_to)
Hope this helps!