access child model in parent validation in rails - ruby-on-rails

I have a rails model that has a child model
class Student
has_many :student_records, dependent: :destroy
accepts_nested_attributes_for :student_records, :allow_destroy => true, reject_if: proc { |attributes| attributes['record'].blank? }
# Now I would like to access the child model here during creating new records, for validation
validate :sum_of_records_has_to_be_less_than_hundred
def sum_of_records_has_to_be_less_than_hundred
#sum = 0
student_records.each do |sr|
#sum += sr.record
end
end
if #sum > 100
errors.add(:base, :sum_of_records_has_to_be_less_than_hundred)
end
end
class StudentRecord
belongs_to :student
end
The problem is student_records.each does not work because student_records is empty but i can see it in the params.What's going on?
Here is a part of the students controller
Class StudentsController
def new
#st = Student.new
#st.student_records.build
end
def create
#student = Studnet.new(student_params)
if #student.save
flash[:success] = t('student_saved')
redirect_to students_url
else
render 'new'
end
end
private
def student_params
params.require(:student).permit(:full_name, .......,
student_records_attributes:
[:id, :record, :_destroy])
end
end

You misplaced the if...end outside the validation method. You don't have to use instance variable either, local variable will do the work.
Try this:
def sum_of_records_has_to_be_less_than_hundred
sum = 0
student_records.each do |sr|
sum += sr.record
end
errors.add(:base, :sum_of_records_has_to_be_less_than_hundred) if sum > 100
end

Can you try this,
change your validate source like this:
def sum_of_records_has_to_be_less_than_hundred
sum = 0
self.student_records.each do |sr|
sum += sr.record
end
if sum > 100
errors.add(:base, :sum_of_records_has_to_be_less_than_hundred)
end
end

Related

Validation with override setter find_or_initialize_by

In a form, I have a multiselect dropdown that I can manually sort. I override the setter method to achieve this. However, when I add validation, I get the presence error saying that "Tags cannot be blank" even when there are tags selected in the dropdown.
params: {tag_ids: ['', '1', '3']}
I think it has something to do with validating before saving which happens in the override function. Any suggestions in how I can make it to run the validations after running the setter method or vice-versa? Thanks.
Controller:
def update
if #article.update(article_params)
redirect_to articles_path
end
end
def article_params
params.require(:article).permit(:title, tag_ids: [])
end
Models:
class Article
has_many: :article_tags
has_many: :tags, through: :article_tags
validates: :tag_ids, presence: true
def tag_ids=(ids)
ids = ids.reject(&:blank?)
self.article_tags.where.not(article_tag_id: ids).destroy_all
ids.each_with_index do |id, idx|
article_tag= self.article_tags.find_or_initialize_by(article_tag_id: id)
if self.new_record?
article_tag.ordinal = idx + 1
else
article_tag.update(ordinal: idx + 1)
end
end
end
def tags
super.joins(:article_tags).order("article_tags.ordinal").distinct
end
end
class ArticleTag
belongs_to: :article
belongs_to: :tag
end

How do I create a page without a model?

I'm working on an app which has many 'Activities'. Each 'Activity' has many 'Ranks'. I'd like each 'Activity' to have a page called grading, where the user can see a list of all of that activity's ranks and conveniently update them. I imagine the URL would be something like http://localhost:3000/activities/21/grading
I'm already using http://localhost:3000/activities/21/edit for its intended purpose.
I don't need a model for gradings, as I don't need to save any grading records.
I know exactly what to put in the view, I'm just unsure what to add to the controller and routes files. Other people have worked on this app but I'm unable to contact them.
Routes
resources :activities do
collection do
get 'scheduled_classes'
end
end
resources :ranks
end
activities_controller
class ActivitiesController < ApplicationController
def new
#activity = Activity.new
#activity.timeslots.build
#activity.ranks.build
end
def create
#activity = current_club.activities.new(activity_params)
if #activity.save
flash[:success] = "New class created!"
redirect_to activity_path(#activity)
else
render 'new'
end
end
def edit
#activity = current_club.activities.find_by(id: params[:id])
#active_ranks = #activity.ranks.where(active: true)
if !#activity.active?
redirect_to activities_path
else
#activity.timeslots.build
end
end
def update
#activity = current_club.activities.find_by(id: params[:id])
if #activity.update_attributes(activity_params)
flash[:success] = "Class updated!"
redirect_to edit_activity_path(#activity)
else
render 'edit'
end
end
def show
#activity = current_club.activities.find_by(id: params[:id])
#active_ranks = #activity.ranks.where(active: true)
if #activity.nil?
redirect_to root_url
elsif !#activity.active?
redirect_to activities_path
end
end
def index
#activities = current_club.activities.all
end
def destroy
#activity = current_club.activities.find_by(id: params[:id])
if #activity.nil?
redirect_to root_url
else
#activity.destroy
flash[:success] = "Class deleted"
redirect_to activities_path
end
end
end
private
def activity_params
params.require(:activity).permit(:name, :active,
:timeslots_attributes => [:id,
:time_start,
:time_end,
:day,
:active,
:schedule],
:ranks_attributes => [:id,
:name,
:position,
:active])
end
end
activity
class Activity < ApplicationRecord
belongs_to :club
has_many :timeslots, dependent: :destroy
accepts_nested_attributes_for :timeslots,:allow_destroy => true
has_many :ranks, dependent: :destroy
has_many :attendances, dependent: :destroy
accepts_nested_attributes_for :ranks
validates :club_id, presence: true
validates :name, presence: true, length: { maximum: 50 }
end
Your routes don't need to have an associated model or resource.
resources :activities do
collection do
get 'scheduled_classes'
end
member do
get :grading
end
end
will match to activities#grading
See https://guides.rubyonrails.org/routing.html#adding-member-routes for more info.
As you want to add a route on a particular activity, you should add member route on the activity like below,
resources :activities do
collection do
get 'scheduled_classes'
end
get :grading, on: :member
end
Apart from this, you have to add method in ActivitiesController for this route like below,
def grading
#activity = Activity.find_by(id: params[:id])
# do more here
end
In view files, you can create grading.html.erb under activities resources and put your view code there.

DB rolls back on create action

I'm trying to create a form with a series of checks to prevent duplicates during the simultaneous creation of three model records: one for the parent (assuming it doesn't exist), one for its child (assuming it doesn't exist), and one for a join table between the child and the User (to allow the User to have their own copy of the Song object).
In the current state of the code, The checks seemingly pass, but
the server logs show ROLLBACK, and nothing gets saved
to the database EXCEPT the parent object (artist).
When I try to use the ids of the object, I get the error undefined method id for nil:NilClass, or "couldn't find object without an ID".
The following code is in my controller:
class SongsController < ApplicationController
before_action :authenticate_user!
def create
#artist = Artist.find_by(name: params[:artist][:name].strip.titleize) #look for the artist
#song = Song.find_by(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize)
if #artist.present? && #song.present?
#user_song = current_user.user_songs.find(#song_id)
if #user_song.present?
render html: "THIS SONG IS ALREADY IN YOUR PLAYLIST"
render action: :new
else
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
end
elsif #artist.present? && !#song.present?
#song = #artist.songs.build(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize, lyrics: params[:artist][:songs_attributes]["0"][:lyrics].strip)
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
elsif !#artist.present?
#artist = Artist.create(name: params[:artist][:name].strip.titleize)
#song = #artist.songs.build(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize, lyrics: params[:artist][:songs_attributes]["0"][:lyrics].strip)
#user_song = UserSong.create(user_id: current_user.id, song_id: #song.id)
redirect_to root_path
else
render html: "SOMETHING WENT WRONG. CONTACT ME TO LET ME KNOW IF YOU SEE THIS MESSAGE"
end
end
def index
#songs = Song.all
end
def new
#artist = Artist.new
#artist.songs.build
#user_song = UserSong.new(user_id: current_user.id, song_id: #song_id)
end
def show
#song_id = params["song_id"]
#song = Song.find(params[:id])
end
def destroy
UserSong.where(:song_id => params[:id]).first.destroy
flash[:success] = "The song has been from your playlist"
redirect_to root_path
end
def edit
#song = Song.find(params[:id])
#artist = Artist.find(#song.artist_id)
end
def update
end
private
def set_artist
#artist = Artist.find(params[:id])
end
def artist_params
params.require(:artist).permit(:name, songs_attributes: [:id, :title, :lyrics])
end
def set_song
#song = Song.find(params["song_id"])
end
end
The models:
class Artist < ApplicationRecord
has_many :songs
accepts_nested_attributes_for :songs, reject_if: proc { |attributes| attributes['lyrics'].blank? }
end
class Song < ApplicationRecord
belongs_to :artist
has_many :user_songs
has_many :users, :through => :user_songs
end
class UserSong < ApplicationRecord
belongs_to :song
belongs_to :user
end
Sorry if I haven't abstracted enough. Not really sure how, given that there's no error message, just a rollback (without any validations present in any of the controllers).
Thanks to #coreyward and his pointing out of the fat-model skinny-controller lemma (never knew that was a thing), I was able to cut the code down and arrive at a solution immediately. In my models, I used validates_uniqueness_of and scope in order to prevent duplication of records. In my controller, I used find_or_create_by to seal the deal.
To whom it may concern, the final code is as follows:
class SongsController < ApplicationController
before_action :authenticate_user!
def create
#artist = Artist.find_or_create_by(name: params[:artist][:name].strip.titleize)
#song = #artist.songs.find_or_create_by(title: params[:artist][:songs_attributes]["0"][:title].strip.titleize) do |song|
song.lyrics = params[:artist][:songs_attributes]["0"][:lyrics].strip
end
#user_song = current_user.user_songs.find_or_create_by(song_id: #song.id) do |user_id|
user_id.user_id = current_user.id
end
redirect_to root_path
end
class Song < ApplicationRecord
validates_uniqueness_of :title, scope: :artist_id
belongs_to :artist
has_many :user_songs
has_many :users, :through => :user_songs
end
class Artist < ApplicationRecord
validates_uniqueness_of :name
has_many :songs
accepts_nested_attributes_for :songs, reject_if: proc { |attributes| attributes['lyrics'].blank? }
end
class UserSong < ApplicationRecord
validates_uniqueness_of :song_id, scope: :user_id
belongs_to :song
belongs_to :user
end

How to restrict current_user from adding more than 3 order_items to a order per time period?

I'm building a store in Rails that has a specific sales model. I need to allow a user to add only 3 items to his order per 30 days. The 30 days counter should start upon adding the first order_item. Once 30 days expires, user would be able to add 3 orders. If 30 days didn't pass and for an example, user adds two order_items he would still be allowed to add one more order_item within 30 days.
So as well if user tries to add more then 3 items to show an error message and disregard saving of the order_items to current_user's order.
I have products, orders, order_items, users. I guess that I should add something to user model but I'm not sure what.
order_items_controller.rb
def create
#order = current_order
#order_item = #order.order_items.new(order_item_params)
#order.user_id = current_user.id
#order.save
session[:order_id] = #order.id
respond_to do |format|
format.js { flash[:notice] = "ORDER HAS BEEN CREATED." }
end
end
private
def order_item_params
params.require(:order_item).permit(:quantity, :product_id, :user_id)
end
end
user.rb
class User < ActiveRecord::Base
has_many :identities, dependent: :destroy
has_many :order
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :omniauthable, :invitable, :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable
end
order_item.rb
class OrderItem < ActiveRecord::Base
belongs_to :product
belongs_to :order
validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
validate :product_present
validate :order_present
before_save :finalize
def unit_price
if persisted?
self[:unit_price]
else
product.price
end
end
def total_price
unit_price * quantity
end
private
def product_present
if product.nil?
errors.add(:product, "is not valid or is not active.")
end
end
def order_present
if order.nil?
errors.add(:order, "is not a valid order.")
end
end
def finalize
self[:unit_price] = unit_price
self[:total_price] = quantity * self[:unit_price]
end
end
order.rb
class Order < ActiveRecord::Base
belongs_to :order_status
has_many :order_items
before_create :set_order_status
before_save :update_subtotal
def subtotal
order_items.collect { |oi| oi.valid? ? (oi.quantity * oi.unit_price) : 0 }.sum
end
private
def set_order_status
self.order_status_id = 1
end
def update_subtotal
self[:subtotal] = subtotal
end
end
carts_controller.rb
class CartsController < ApplicationController
def show
#order_items = current_order.order_items
end
routes.rb
resources :order_items, only: [:create, :update, :destroy, :new]
form.html.erb
<%= form_for OrderItem.new, html: {class: "add-to-cart"}, remote: true do |f| %>
<div class="input-group">
<%= f.hidden_field :quantity, value: 1, min: 1 %>
<div class="input-group-btn">
<%= f.hidden_field :product_id, value: product.id %>
<%= f.submit "Add to Cart", data: { confirm: 'Are you sure that you want to order this item for current month?'}, class: "btn btn-default black-background white" %>
</div>
</div>
<% end %>
</div>
I would add a begin_date and a order_counter to user model. Every time you add an order, look if the begin_date is more than 30 days ago, then set the begin_date to the actual date. If the begin_date is less than 30 days ago, increase the counter. And if the counter ist already 3 refuse the order.
You can add the columns to the user table by the command line argument
rails generate migration AddOrderCounterToUser
This will create a class in db/migrations:
class AddPartNumberToProducts < ActiveRecord::Migration
def change
add_column :users, :begin_date, :date
add_column :users, :order_counter, :integer
end
end
Add the additional attributes in your UserController to permit them in user_params.
Then change the create method in your OrderItemController
def create
now = Date.today
success = false
if current_user.begin_date && ((now - 30) < current_user.begin_date)
if current_user.order_counter >= 3
# deal with the case that order should not be created,
# for example redirect.
else
current_user.order_counter += 1
current_user.save
success = true
end
else
current_user.order_counter = 1
current_user.begin_date = now
current_user.save
success = true
end
if success
#order = current_order
#order_item = #order.order_items.new(order_item_params)
#order.user_id = current_user.id
#order.save
session[:order_id] = #order.id
respond_to do |format|
format.js { flash[:notice] = "ORDER HAS BEEN CREATED." }
end
else
respond_to do |format|
format.js { flash[:notice] = "CREATION NOT POSSIBLE." }
end
end
end
You can also put the checking code in a method in the user model, that would be cleaner.
Generally, when you don't want to create an element in rails under certain circumstances, you should choose to handle the situation via validators.
You could take a nesting approaches here:
Nest your OrderItem routes under Order (you can find further information about nesting in the Rails Guides about Nested Routing)
You should start by adding a new database column first_item_added_at to you Order model
rails generate migration AddFirstItemAddedAtToOrder
class AddFirstItemAddedAtToOrder < ActiveRecord::Migration
def change
add_column :orders, :first_item_added_at, :date
end
end
When nesting, you would create a new OrderItem via the route
POST /orders/:id/order_items
Then, you have to add a validator to your OrderItem model
class OrderItem < ActiveRecord::Base
validate :only_3_items_in_30_days
private
def only_3_items_in_30_days
now = Date.new
days_since_first = now - order.first_item_added_at
if order.order_items.count > 2 && days_since_first < 30
errors.add(:base, 'only 3 items in 30 days are allowed')
end
true # this is to make sure the validation chain is not broken in case the check fails
end
end
Now your controller only needs to create a new item and save it
def create
#item = OrderItem.new(item_params)
if #item.save
render <whatever_you_want_to_render>
else
# #item will contain the errors set in the model's validator
render <error_reaction>
end
end
private
def item_params
params.require(:order_item).permit(
:attribute_1,
:attribute_2,
:order_id # << this one is very important
)
end
If you don't wish to nest OrderItem, than the model still remains the same, but your controller would look like:
def create
#item = OrderItem.new(order_item_params)
session[:order_id] = current_order.id
if #item.save
respond_to do |format|
format.js { flash[:notice] = "ORDER HAS BEEN CREATED." }
end
else
render <handling for error>
end
end
private
def order_item_params
base_params = params.require(:order_item)
.permit(:quantity, :product_id, :user_id)
base_params.merge(order: current_order)
end
Please note, that I added current_order.id to your order_item_params method.
EDIT: replaced order_id: current_order.id by order: current_order to provide the relation to the new OrderItem before it is actually saved

Rails 4: Undefined method `total_price' for nil:NilClass, Order Controller

I'm having trouble having order go through. I have posted the error bellow. I think the issue has to do with the create method in the OrderController.rb, I do have the total_price method already defined but.. other than that I'm not sure how to fix the issue. Any help would be appreciated. Thank you.
class OrderTransaction
def initialize order, nonce
#order = order
#nonce = nonce
end
def execute
#result = Braintree::Transaction.sale(
amount: order.total_price,
payment_method_nonce: nonce
)
end
def ok?
#result.success?
end
private
attr_reader :order, :nonce
end
class Order < ActiveRecord::Base
belongs_to :user
has_many :order_items
def total_price
order_items.inject(0) { |sum, item| sum + item.total_price }
end
end
class OrdersController < ApplicationController
before_filter :initialize_cart
def index
#orders = Order.order(created_at: :desc).all
end
def create
#order_form = OrderForm.new(
user: User.new(order_params[:user]),
cart: #cart
)
if #order_form.save
notify_user
if charge_user
redirect_to root_path, notice: "Thank you for placing the order."
else
flash[:warning] = <<EOF
Your order ID is #{#order_form.order.id}.
<br/>
Something went wrong.
EOF
redirect_to new_payment_order_path(#order_form.order)
end
else
render "carts/checkout"
end
end
def update
#order = Order.find params[:id]
#previous_state = #order.state
if #order.update state_order_params
notify_user_about_state
redirect_to orders_path, notice: "Order was updated."
end
end
def new_payment
#order = Order.find params[:id]
#client_token = Braintree::ClientToken.generate
end
def pay
#order = Order.find params[:id]
transaction = OrderTransaction.new #order, params[:payment_method_nonce]
transaction.execute
if transaction.ok?
redirect_to root_path, notice: "Thank you for placing the order."
else
render "orders/new_payment"
end
end
private
def notify_user
#order_form.user.send_reset_password_instructions
OrderMailer.order_confirmation(#order_form.order).deliver
end
def notify_user_about_state
OrderMailer.state_changed(#order, #previous_state).deliver
end
def order_params
params.require(:order_form).permit(
user: [ :name, :phone, :address, :city, :country, :postal_code, :email ]
)
end
def charge_user
transaction = OrderTransaction.new #order, params[:payment_method_nonce]
transaction.execute
transaction.ok?
end
def state_order_params
params.require(:order).permit(:state)
end
end
class OrderItem < ActiveRecord::Base
belongs_to :order
belongs_to :product
def total_price
self.quantity * self.product.price
end
end
class OrderForm
include ActiveModel::Model
attr_accessor :user, :order # credit_card
attr_writer :cart
def save
set_password_for_user
if valid?
persist
true
else
false
end
end
def has_errors?
user.errors.any?
end
private
def valid?
user.valid?
end
def persist
user.save
#order = Order.create! user: user
build_order_items
end
def set_password_for_user
user.password = Digest::SHA1.hexdigest(user.email + Time.now.to_s)[0..8]
end
def build_order_items
#cart.items.each do |item|
#order.order_items.create! product_id: item.product_id, quantity: item.quantity
end
end
end
class OrderItem < ActiveRecord::Base
belongs_to :order
belongs_to :product
def total_price
self.quantity * self.product.price
end
end
As a standard note, any NilClass error basically means you haven't defined the variable you're trying to manipulate.
The key to solving the problem is to therefore find why the variable isn't defined, and populate it.
def execute
#result = Braintree::Transaction.sale(
amount: order.total_price,
payment_method_nonce: nonce
)
end
This is where Rails says the variable is not populated.
However, as with many problems in programming, the cause of the issue may not be as defined...
I initially thought the problem was that you weren't calling #order. However, the class initializes with order, so that shouldn't be a problem. So you have to look at how you're invoking the class:
transaction = OrderTransaction.new #order, params[:payment_method_nonce]
This surmises that #order is defined.
I surmise it isn't.
Here's what I'd do:
def create
#order_form = OrderForm.new(
user: User.new(order_params[:user]),
cart: #cart
)
if #order_form.save
notify_user
#order = #order_form.order #-> not efficient but should create #order
if charge_user
redirect_to root_path, notice: "Thank you for placing the order."
else
flash[:warning] = <<EOF
Your order ID is #{#order_form.order.id}.
<br/>
Something went wrong.
EOF
redirect_to new_payment_order_path(#order_form.order)
end
else
render "carts/checkout"
end
end
Personally, I think this highlights a deeper problem with your code structure:
You're creating an OrderForm object and yet processing #order_form.order
Your controller is full of tiny methods which bloat it up big time
Your controller is for orders, yet builds OrderForm objects
I'd do my best to make my controller as thin as possible:
#app/controllers/orders_controller.rb
class OrdersController < ApplicationController
def new
#order = current_user.order.new
end
def create
#order = current_user.order.new order_params
if #order.save
#order.charge
end
end
private
def order_params
params.require(:order).permit(:x, :y, :z, order_products_attributes: [:product, :qty])
end
end
I'd have a more modular model structure:
#app/models/order.rb
class Order < ActiveRecord::Base
belongs_to :user
has_many :order_products
has_many :products, through: :order_products, extend ProductQty
has_many :payments, inverse_of: :order
scope :cart, -> { order_products }
def total_price
products.pluck(:price, :qty) #-> need to work out
end
def charge
payment = payments.create
payment.execute ? payment.success : payment.error #-> something conditional
end
end
#app/models/order_product.rb
class OrderProduct < ActiveRecord::Base
#columns id | order_id | product_id | qty | created_at | updated_at
belongs_to :order
belongs_to :product
end
#app/models/payment.rb
class Payment < ActiveRecord::Base
belongs_to :order, inverse_of: :payments
def execute
Braintree::Transaction.sale(amount: order.total_price)
end
end
#app/models/product.rb
class Product < ActiveRecord::Base
has_many :order_products
has_many :orders, through: :order_products
end
#app/models/concerns/product_qty.rb
module ProductQty
#Load
def load
products.each do |qty|
proxy_association.target << qty
end
end
#Private
private
#Products
def products
return_array = []
through_collection.each_with_index do |through,i|
associate = through.send(reflection_name)
associate.assign_attributes({qty: items[i]})
return_array.concat Array.new(1).fill( associate )
end
return_array
end
#######################
# Variables #
#######################
#Association
def reflection_name
proxy_association.source_reflection.name
end
#Foreign Key
def through_source_key
proxy_association.reflection.source_reflection.foreign_key
end
#Primary Key
def through_primary_key
proxy_association.reflection.through_reflection.active_record_primary_key
end
#Through Name
def through_name
proxy_association.reflection.through_reflection.name
end
#Through
def through_collection
proxy_association.owner.send through_name
end
#Captions
def items
through_collection.map(&:qty)
end
#Target
def target_collection
proxy_association.target
end
end
I wanted to include cart somewhere, I'll have to do that another time.
For now, you'd be able to do the following:
#order = current_user.orders.find params[:id]
#order.products.each do |product|
product.qty #-> 5
#order.total_price #-> prices * qtys
--
This is not complete or tested, but I hope it shows you how you could improve your code structure dramatically, by making it modular. IE keep as many methods tied to your objects as possible.
In short, you should be able to do the following:
#order = current_users.orders.find params[:id]
if #order.payments.any?
#payment = #order.payment.first
#payment.success?
end
The problem is in your charge_user method inside OrdersController class where you call this code:
transaction = OrderTransaction.new #order, params[:payment_method_nonce]
you don't really defined #order in this method, i.e. #order is nil here and that's causing the problem for you here and you are getting this error: undefined method total_price for nil:NilClass
Set #order value inside the charge_user method before you call this line of code and make sure #order is NOT nil:
transaction = OrderTransaction.new #order, params[:payment_method_nonce]
One possible solution is to modify your charge_user method to take an order argument like this:
def charge_user(order)
transaction = OrderTransaction.new order, params[:payment_method_nonce]
transaction.execute
transaction.ok?
end
And, in your create method call like this:
if charge_user(#order_form.order)
redirect_to root_path, notice: "Thank you for placing the order."
else
# rest of the code
end
This will solve your issue.

Resources