I'm building a blog app on rails, so i created a method to update the number of posts when a new post is added by a certain user (user and post are my models).
class Post < ApplicationRecord
belongs_to :author, class_name: 'User', foreign_key: 'user_id'
has_many :comments
has_many :likes
after_save :update_user_counter
validates :title, presence: true, length: { maximum: 250 }
validates :comments_counter, :likes_counter, numericality: { only_integer: true, greater_than_or_equal_to: 0 }
after_initialize do |post|
post.comments_counter = 0
post.likes_counter = 0
end
def update_user_counter
author.increment!(:posts_counter)
end
def recent_5_comments
comments.order(created_at: :desc).limit(5)
end
private :update_user_counter
end
I test the method using rails console as follows
I start to review the previously created user posts_counter values you can see they start in 0
I create a variable to store the first user using User.find(1)
I create a new post to that user
4.When runing the last step the posts_counter gets updated and shows 1 when variable is called
5.But when running User.all the value is again 0
Is there a way to fix this? I need to be able to see the changes when i run User.all I have tried changing my method but it seems it does not change anything, I'm sorry for the extension of the question I'm starting learning rails and this is frustrating.
Related
I am building a simple expenses management app on rails 5.1.4. I am using the following five models.
Payees
class Payee < ApplicationRecord
has_many :expenses
validates :title, uniqueness: true, presence: true
end
Accounts
class Account < ApplicationRecord
before_save :update_balance
validates :balance, numericality: { greater_than_or_equal_to: 0 }
has_many :expenses
end
Budgets
class Budget < ApplicationRecord
belongs_to :categories
has_many :expenses, through: :categories
end
Categories
class Category < ApplicationRecord
validates :title, uniqueness: true, presence: true
has_many :expenses
has_one :budget
end
Expenses
class Expense < ApplicationRecord
belongs_to :categories
belongs_to :budgets
belongs_to :payees
belongs_to :accounts
validates :title, :value, presence: true
before_save :default_account
end
When I try to create a new expense I am facing a validation error
Validation failed: Categories must exist, Budgets must exist, Payees must exist, Accounts must exist
The issue is that all the above records exist. To explain my self let's say I am passing the params account_id: 1, payee_id: 1, category_id: 1. If I do:
Account.find(1) #=> Finds the record
Category.find(1) #=> also ok
Payee.find(1) #=> also ok
I am aware of the solution referred in this question (adding optional: true) but I don't get why I should do that while all of the above exist
Edit
The code that is raising the error is:
def create
#expense = Expense.create!(title: params[:expense]['title'],
value: params[:expense]['value'],
date: params[:expense]['date'],
comment: params[:expense]['comment'],
payee_id: params[:expense]['payee_id'],
category_id: params[:expense]['category_id'],
account_id: params[:expense]['account_id'])
end
The parameters that are passed through the form are
{"utf8"=>"✓",
"authenticity_token"=>"DWd1HEcBC3DhUahfOQcdaY0/oE+VHapxxE+HPUb0I6iSiqMxkz6l+vlK+1zhb66HnZ/vZRUVG4ojTdWUCjHtGg==",
"expense"=>{"title"=>"test", "value"=>"-20", "category_id"=>"1", "payee_id"=>"2", "date"=>"2018-01-21", "account_id"=>"1", "comment"=>""},
"commit"=>"Submit"}
I would first start by commenting out all your model validations, then creating an expense. Add back one model validation at a time, each time test creating an expense to see what validation is causing the error.
also you may want to change how you're creating the expense to something like below.
change your controllers create action to
def create
#expense = Expense.new(expense_params)
if #expense.save
flash[:success] = "expense created"
redirect_to expense_url(#expense.id)
else
render 'new'
end
end
next under your private method at the bottom of your controller you want to do something like this
private
# Never trust parameters from the scary internet, only allow the white list through.
def expense_params
params.require(:expense).permit(:title, :value, :date, etc...)
end
I finally found out where the problem is! It was the naming of the classes/models that raised the error. I had named my models on singular (Account, Category, etc) while all references are searching for plurals ( Accounts, Categories, etc). I had to re-do all migrations from the very beginning in order to make it work the proper way!
Thanks to everyone for spending the time though!
I'm building a quick Rails project that allows users to manage their email addresses. Users can have many emails, but one (and only one) of those emails has to be marked as 'primary' (for login), and a user cannot exist without a primary email.
I've been struggling to get this to work right - it seems so circular to me. I need to build a User, and then the Email, but I don't want to save the User into the database unless the Email is valid, which it won't be until the User is saved (because of the validates :user, presence: true constraint).
Accepts nested resources for doesn't seem to work with .new (works fine with .create), and if my Email fails its validations, the User still shows as valid.
Been having a difficult time trying to find good resources (or SO questions) for building/validating multiple/dependent models from a single form.
What's the most Rails way to do this?
User
has_many :emails
has_one :primary_email, -> { where(primary: true) }, class_name: "Email"
accepts_nested_attributes_for :primary_email
validates :first_name, presence: true
validates :last_name, presence: true
validates :birthday, presence: true
validates :password_digest, presence: true
Email
belongs_to :user
validates :user, presence: true
validates :address, presence: true, uniqueness: {
case_sensitive: false
}
UsersController
def new
#user = User.new
end
def create
#user = User.new(user_params)
if #user.save
# do something
else
# show #user.errors
end
end
private
def user_params
params.require(:user).permit(
:first_name,
:last_name,
:birthday,
:password,
:password_confirmation,
:primary_email_attributes => [:address]
)
end
EDIT
The Email model also contains the following fields:
label = string, eg. 'Personal', 'Work', etc
primary = boolean, whether it's marked as primary email or not
confirmation_code = autogenerated on creation, used to confirm ownership
confirmed = boolean, whether it's been confirmed or not
class User
user has_many :emails
user has_one :primary_email, -> { where(primary: true) }, class_name: "Email", autosave: true
after_initialize {
build_primary_email if new_record?
}
end
class Email
# use gem https://github.com/balexand/email_validator
validates :my_email_attribute, :email => true
end
So after a user initialized its building a primary_email so that record is already associated, or at least it will be if it can be saved. the autosave is working pretty cool - if the primary-email can't be saved due validation error, the user can't neither. should work out of the box, im in a bus right now, can't check it. cheers
futher information: http://api.rubyonrails.org/classes/ActiveRecord/AutosaveAssociation.html
If validations for any of the associations fail, their error messages will be applied to the parent. That means, the Parent Model (in your case User) is having errors, and thats why the saving is not possible! that's what you are looking for.
I would store a primary email as a common field and additional emails some another way. I would prefer to store additional emails in another field too that is Array rather than in an associated table. You shouldn't store a primary email in another table. Just imagine, every time you need authorize user or just get his email you will perform an extra request to db.
Meant to post this months ago.
The solution, keeping users and emails normalized across different models without storing a primary email as an attribute on the user, is to use inverse_of:
User.rb
class User < ActiveRecord::Base
has_many :emails, inverse_of: :user, dependent: :destroy
accepts_nested_attributes_for :emails
validates :emails, presence: true
end
Email.rb
class Email < ActiveRecord::Base
belongs_to :user, inverse_of: :emails
validates :user, presence: true
end
This allows validations to be performed using in-memory objects, rather than via database calls (ie the object associations are being validated, rather than the presence of an id/record in the database). Therefore they both pass validation and can both be saved in the same transaction.
See: https://viget.com/extend/exploring-the-inverse-of-option-on-rails-model-associations
I just studied ruby on rails today and I'd like some help on creating proper associations.
I have these models:
Comment:
class Comment < ActiveRecord::Base
belongs_to :stammr_post
validates :stammr_post_id, presence: true
validates :content, presence: true
end
Post:
class StammrPost < ActiveRecord::Base
has_many :comments, :dependent => :destroy
validates :content, presence: true
end
The thing is, whenever I create a Comment, and I enter a Stammr_post_id that doesn't exist, rails still accepts it as valid. Isn't that supposed to be invalid since a comment belongs to Stammr_post? The stammr_post should exist first before a comment could be made. How do I resolve this? Is it supposed to be automatic? Did I make a typo somewhere? Or do I need to do manual validation for that? Sorry, I'm kinda new to Ruby on Rails. I'm a former grails developer and I was used to the automatic associations thing. #_#
The correct way to do this is create the Comment through the parents association. That way you are taking advantage of the association;
So instead of doing this;
#comment = Comment.new(:stammr_post_id => 123)
#comment.save
do this;
# Find the StammrPost first. You may want to replace params[:stammr_post_id]
# with your StammrPost id
#stammr_post = StammrPost.find(params[:stammr_post_id])
#comment = #stammr_post.comments.build()
#comment.save
You could validate associated belongs_to object (stammr_post) instead of database column (stammr_post_id).
class Comment < ActiveRecord::Base
belongs_to :stammr_post
validates :stammr_post, :content, presence: true
end
I want to order posts based on the total votes it has. This is what I have in the Post Model:
class Post < ActiveRecord::Base
attr_accessible :title, :url
validates :title, presence: true
validates :url, presence: true
has_many :votes
def vote_number
votes.where(direction: "up").count - votes.where(direction: "down").count
end
end
And this is what I attempted to do in the Post Controller:
def index
#posts = Post.last(10).order('vote_number')
end
Nevertheless I get this error from the index:
undefined method `order' for #<Array:0x3787158>
The other questions in Stack Overflow resolved this problem by making the calculation in the Post Controller but I can not do it because votes are arrays and not integers.
Found a way to solve it. Instead of using order I used sort_by.
Instead of having this in the Post Controller:
def index
#posts = Post.last(10).order('vote_number')
end
I used sort_by:
def index
#posts = Post.all.sort_by{|post|-post.vote_number}
end
You should try counter cache.
You can read more about it from the following links -
How to sort authors by their book count with ActiveRecord?
http://hiteshrawal.blogspot.com/2011/12/rails-counter-cache.html
http://railscasts.com/episodes/23-counter-cache-column
Counter cache only works inside rails. If you updated from outside application you might have to do some work around.
first, last and all execute query. Insert order always before those three.
class Post < ActiveRecord::Base
attr_accessible :title, :url
attr_reader :vote_difference # getter
attr_writer :vote_difference # setter
validates :title, presence: true
validates :url, presence: true
has_many :votes
end
class Vote < ActiveRecord::Base
belongs_to :post, :counter_cache => true
#more class methods here
def after_save
self.update_counter_cache
end
def after_destroy
self.update_counter_cache
end
def update_counter_cache
post.vote_difference = post.comments.where(direction: 'up').count - post.comments.where(direction: 'down').count
post.save
end
end
Now you can sort by vote_difference when you query up.
for example -
posts = Post.order(:vote_difference, :desc)
I haven't check the correctness of my code yes. If you find any issues please let me know. I am sure it can be adapted to make it works.
If you follow this pattern to use counter_cache you might will to run a migration to add a vote_difference column, and another migration to update the vote_difference column for previous created post.
I have a basic invoice setup with models: Invoice, Item, LineItems.
# invoice.rb
class Invoice < ActiveRecord::Base
has_many :line_items, :dependent => :destroy
validates_presence_of :status
before_save :default_values
def default_values
self.status = 'sent' unless self.status
end
end
# item.rb
class Item < ActiveRecord::Base
has_many :line_items
validates_presence_of :name, :price
end
# line_item.rb
class LineItem < ActiveRecord::Base
belongs_to :item
belongs_to :invoice
before_save :default_values
validates_presence_of :invoice_id
validates :item_id, :presence => true
end
There is more in the model but I only presented the above for simplicity.
I get the following errors:
2 errors prohibited this invoice from being saved:
Line items invoice can't be blank
Status can't be blank
So two problems:
If I remove validates :invoice_id, :presence => true I don't get the Line items invoice can't be blank error message anymore, but why? I do want to validate the invoice_id on line_items, ALL line_items are supposed to have an invoice_id. How can I validate the invoice_id on line_items without getting an error?
Why am I getting the Status can't be blank error if I set it as a default value? I can probably set it up on the invoices_controller but I think the default value should be set in the model, right? How can I validate the presence of status and still have a default value in the model for it?
Both of these validation errors are occurring because validations get called before save (and before the before_save callback).
I'm assuming that you're using a nested_form to create the invoice and it's line items at the same time. If this is the case, you don't want to validates :invoice_id, :presence => true on the line items - the invoice and the line items are coming in at the same time, and the invoice hasn't been saved yet, so it doesn't have an id. If you leave the validation in, you'll need to create and save an empty invoice first, and then create the line items later so the invoice_id is available. If you only want to make sure invoice_id is still set after any edits, you can enforce this via validates :invoice_id, :presence => true, :on => :update this will skip the validation when the line item is being created (and the invoice_id isn't available yet).
You're running into problems with validates :status, :presence => true for similar reasons - the values coming in via the request are being validated against, and the "status" value isn't there. The before_save callback runs after validation. You can set the default value in the before_validation or after_initialization callback and the values will be there when validations are run.
Check out the Callbacks documentation for Rails for more info.
I'll start with 2:
before save is being executed only before save, meaning, after the object passed validation and is about to be saved. If the validation fails - it won't be executed.
as for 1:
Can you give an example of how you're trying to create an invoice?
Problem 1
Try validates_associated which checks that the associated models are all valid
Problem 2
Like most of the answers say before_save gets called after validations. The magic you're looking for is after_initialize which gets run after an object's initialize method is called.
class Invoice < ActiveRecord::Base
after_initialize :default_values
validates :status, presence: true
private
def default_values
self.status ||= 'sent'
end
end