Joint query across 2 models (has_many) - ruby-on-rails

Hi I need help and all insight appreciated. I have two models Auctions and Bids and I want to retrieve the All auctions current_user won, the ones s/he has been outbid on and the ones s/he's winning
Here are the two models:
class Auction < ActiveRecord::Base
extend FriendlyId
friendly_id :guid, use: :slugged
before_save :populate_guid
mount_uploaders :images, ImageUploader
belongs_to :client
has_many :bids, dependent: :destroy
has_one :order, dependent: :destroy
validates_presence_of :title, :lien_price,
:end_time, :collateral_value,
:redemption_date, :current_interest_rate,
:additional_tax, :collateral_details,
:location, :client_id, :starting_bid
validate :end_time_in_the_future, :on => :update
validates_uniqueness_of :guid, case_sensitive: false
def end_time_in_the_future
errors.add(:end_time, "can't be in the past") if self.end_time && self.end_time < Time.now
end
def self.get_active_auctions
where("end_time > ?", Time.now)
end
def self.closed_auctions
where("end_time < ?", Time.now)
end
def highest_bid
self.bids.maximum("amount")
end
def highest_bid_object
self.bids.order(:amount => :desc).limit(1).first
end
def highest_bidder
self.highest_bid_object.user if highest_bid_object
end
def closed?
self.end_time < Time.now
end
private
def populate_guid
if new_record?
while !valid? || self.guid.nil?
self.guid = SecureRandom.random_number(1_000_000_000).to_s(36)
end
end
end
end
and
class Bid < ActiveRecord::Base
extend FriendlyId
friendly_id :guid, use: :slugged
belongs_to :auction
belongs_to :user
before_save :populate_guid
validates_presence_of :amount, :user_id,
:auction_id
#validate :higher_than_current?
validates :amount, :numericality => true
validates_uniqueness_of :guid, case_sensitive: false
def higher_than_current?
if !Bid.where("amount > ? AND auction_id = ?", amount, self.auction.id).empty?
errors.add(:amount, "is too low! It can't be lower than the current bid, sorry.")
end
end
private
def populate_guid
if new_record?
while !valid? || self.guid.nil?
self.guid = SecureRandom.random_number(1_000_000_000).to_s(36)
end
end
end
end
I thought
#auctions = Auction.closed_auctions.where(highest_bidder: current_user)
or
#auctions = Auction.closed_auctions.joins(:bids).where(highest_bidder: current_user)
would work but they both raise an error.
Edit this works
#auctions = Auction.closed_auctions.references(highest_bidder: current_user)
But there's probably a better way.

You probably can't access current_user from controller (devise?). So you need to pass the user as a parameter to the class or instance method. What you should look into are scopes and especially scopes that accept parameters. Scopes could really help you refactor your Auction model (you really don't need any methods that only return a where()), but also solve the inaccessible current_user.
Use it like this in your Auction model:
scope: :highest_bidder -> (current_user) { where(highest_bidder: current_user) }
And call it like this from your controller:
#auctions = Auction.closed_auctions.highest_bidder(current_user)

Related

Rails use an instance variable in controller or generate within a helper?

So I'm trying to build out on an Invoice page the past_due_amount where I'm trying to find only the invoices for the current account, that are not paid off, and should be in the past.
So roughly I have:
past_due_amount = Invoice.where(account: invoice.account, status: :unpaid).where('date < ? ', invoice.date).map(&:due).sum
For additional context here are the models involved:
Invoice:
class Invoice < ApplicationRecord
belongs_to :account
has_many :line_items, dependent: :destroy
has_many :payment_destinations, dependent: :destroy
has_many :prorated_fees, dependent: :nullify
enum status: [:unpaid, :paid]
validates :date, presence: true
validates :period_start, :period_end,
uniqueness: { scope: :account, allow_blank: true }, on: :create
validate :start_is_before_end
DAYS_DUE_AFTER_DATE = 14.days
scope :descending, -> { order(date: :desc) }
scope :ascending, -> { order(date: :asc) }
scope :due, -> { unpaid.where(arel_table[:date].lteq(Time.zone.today - DAYS_DUE_AFTER_DATE)) }
def total
if persisted?
line_items.sum(:amount)
else
line_items.map(&:amount).sum
end
end
end
Account:
class Account < ApplicationRecord
belongs_to :customer
belongs_to :property_address,
class_name: Address.to_s,
dependent: :destroy,
required: false
[:products, :account_changes, :equipments,
:payments, :invoices].each do |assoc|
has_many assoc, dependent: :destroy
end
accepts_nested_attributes_for :property_address
delegate :street, :city, :state, :zip,
to: :property_address, allow_nil: true
delegate :email, :full_name, to: :customer
enum status: [:staged, :active, :inactive]
scope :active_or_staged, -> { where(status: [:staged, :active]) }
scope :past_due, lambda {
joins(:invoices)
.where(
Invoice.arel_table[:status].eq(:unpaid)
.and(Invoice.arel_table[:date].lt(Time.zone.today - 14.days))
).distinct
}
scope :search, lambda { |term|
joins(:customer)
.where(
arel_table[:account_num].matches("%#{term}%")
.or(Customer.arel_search(term))
)
}
end
With the rough code in place I decided to build out a instance variable on the InvoicesController within the show method as below:
def show
#invoice = Invoice.find_by!(id: params[:id], account: current_customer.account_ids)
#account = #invoice.account
#past_due_amount = Invoice.where(account: #account, status: :unpaid).where('date < ?', #invoice.date).map(&:due).sum
end
No errors appear but that's not saying much since the examples I have are poor, at best. But my question is...should I actually be putting this in a helper instead of the show method on an InvoicesController or even in the model?
EDIT:
I've also tried putting in my Invoice model:
def self.past_due_amount
Invoice.where(account: #account, status: :unpaid).where('date < ?', #invoice.date).map(&:due).sum
end
Then in my InvoicesController:
def show
#invoice = Invoice.find_by!(id: params[:id], account: current_customer.account_ids)
#account = #invoice.account
#past_due_amount = Invoice.past_due_amount
end
End up getting undefined method `date' for #invoice.date.
The best way is to create a method past_due_amount in the InvoicesHelper
module InvoicesHelper
def past_due_amount
Invoice.where(account: #account, status: :unpaid).where('date <?', #invoice.date).map(&:due).sum
end
end
In you controller just initialize all the instance variables
def show
#invoice = Invoice.find_by!(id: params[:id], account: current_customer.account_ids)
#account = #invoice.account
end
In the view you should use: <%= past_due_amount > to show your data
Create an instance method in Account model
def past_due_amount
invoices.map(&:due).sum
end
and then from view you can all it #account.past_due_amount. no need to create extra instance variable in controller action
So I sort of used Patrick's answer but it was actually failing so I switched to passing invoice as params.
Helper
module InvoicesHelper
def past_due_amount(invoice)
Invoice.where(account: invoice.account, status: :unpaid).where('date < ?', invoice.date).map(&:due).sum
end
end
Then in my view:
<% if past_due_amount(invoice).positive? %>
<p><%= number_to_currency past_due_amount(invoice) %></p>
<% end %>

Multisteps form creation with relationships

I'm trying to implement a multistep form following this article: https://medium.com/#nicolasblanco/developing-a-wizard-or-multi-steps-forms-in-rails-d2f3b7c692ce
The problem is that my model has relationships and these are not recognized in the step 3 presenting an error, see below my full code and the error message:
class Evaluation < ApplicationRecord
belongs_to :user
belongs_to :teacher
belongs_to :school
belongs_to :subject
has_many :evaluation_tags
has_many :tags, through: :evaluation_tags
accepts_nested_attributes_for :evaluation_tags
validates :teacher_id, presence: true
validates :subject_id, presence: true
validates :school_id, presence: true
validates :user_id, presence: true
validates :rating, presence: true
end
module Wizard
module Evaluation
STEPS = %w(step1 step2 step3).freeze
class Base
include ActiveModel::Model
attr_accessor :evaluation
delegate *::Evaluation.attribute_names.map { |attr| [attr, "#{attr}="] }.flatten, to: :evaluation
def initialize(evaluation_attributes)
#evaluation = ::Evaluation.new(evaluation_attributes)
end
end
class Step1 < Base
validates :teacher_id, presence: true
end
class Step2 < Step1
validates :subject_id, presence: true
end
class Step3 < Step2
validates :school, presence: true
validates :user, presence: true
validates :rating, presence: true
end
end
end
class EvaluationsController < ApplicationController
before_action :complete_sign_up, except: [:index]
# before_action :load_evaluation_wizard, except: %i(validate_step)
before_action :load_evaluation_wizard, except: [:validate_step, :new]
def step1
#teachers = Teacher.includes(:schools).where(schools: {id: current_user.student_details.last.school_id}).order(full_name: :ASC)
end
def step2
#teacher_id = session[:evaluation_attributes]["teacher_id"]
#subjects = Subject.includes(:teachers, :schools).where(teachers: {id: #teacher_id}).where(schools: {id: current_user.student_details.last.school_id}).order(name: :ASC)
end
def step3
pp session[:evaluation_attributes]
end
def validate_step
current_step = params[:current_step]
#evaluation_wizard = wizard_evaluation_for_step(current_step)
#evaluation_wizard.evaluation.attributes = evaluation_wizard_params
session[:evaluation_attributes] = #evaluation_wizard.evaluation.attributes
# pp session[:evaluation_attributes]
if #evaluation_wizard.valid?
next_step = wizard_evaluation_next_step(current_step)
create and return unless next_step
redirect_to action: next_step
else
render current_step
end
end
def load_evaluation_wizard
#evaluation_wizard = wizard_evaluation_for_step(action_name)
end
def wizard_evaluation_next_step(step)
Wizard::Evaluation::STEPS[Wizard::Evaluation::STEPS.index(step) + 1]
end
def wizard_evaluation_for_step(step)
raise InvalidStep unless step.in?(Wizard::Evaluation::STEPS)
"Wizard::Evaluation::#{step.camelize}".constantize.new(session[:evaluation_attributes])
end
def evaluation_wizard_params
params.require(:evaluation_wizard).permit(:teacher_id, :subject_id, evaluation_tags_attributes: {tag_ids: []}).merge(user: current_user, school: current_user.student_details.last.school)
end
class InvalidStep < StandardError; end
end
#STEP3.HTML.ERB
<%= f.collection_check_boxes(:tag_ids, Tag.all, :id, :name) do |tag| %>
<%= tag.label(class: "tags tags-bom") { tag.check_box(class: "checkbox_tags") + tag.text} %>
<% end %>
#ERROR
undefined method `tag_ids' for #<Wizard::Evaluation::Step3:0x00007fadb2be6a88>
How can i make that module recognizes the relationships?
I think you will need a custom validation for tag_ids. I don't think rails will validate multiple items in one step. Maybe add a custom validation at the bottom of your application controller:
def validate_tag_ids
if !tag_ids.is_a?
errors.add(:tag_ids, :invalid)
end
end
And then use:
validates: :validate_tag_ids
instead of:
validates :tag_ids, presence: true

NoMethodError at / undefined method `name' for nil:NilClass

I have tried many solution and came up with good one but still getting error. I am editing my whole question.
I am trying to create Friendly URL with friendly_id gem.
In my project First user need to signup with devise.
Devise will pass some information to profile model with
model/user.rb
delegate :age, :city, :region, :country, to: :profile
I want to make user.name to be Friendly_id candidate. I have tried following code in my Profile model:-
class Profile < ActiveRecord::Base
extend FriendlyId
friendly_id :user_name , use: :slugged
def user_name
user.name
end
But it is giving error
NoMethodError at /
undefined method `name' for nil:NilClass
now After submitting user form.
Please suggest possible solution with explanation.
My User.rb looks like
require 'open-uri'
class User < ActiveRecord::Base
paginates_per 10
validates :name , presence: true, length: { maximum: 200 }
scope :by_name, ->(name) do
joins(:profile).where('lower(name) LIKE ?', "%#{name.downcase}%")
end
delegate :age, :city, :region, :country, to: :profile
has_one :profile, dependent: :destroy
accepts_nested_attributes_for :profile
def self.new_with_session(params, session)
session_params = { 'profile_attributes' => {} }
provider = session['devise.provider']
if provider && data = session["devise.#{provider}"]
session_params['name'] = data[:name] if data[:name]
session_params['email'] = data[:email] if data[:email]
session_params['profile_attributes'] =
{ avatar: data[:image] } if data[:image]
end
params.deep_merge!(session_params)
super.tap do |user|
if auth = Authorization.find_from_session(session, provider)
user.authorizations << auth
end
end
end
def password_required?
super && registered_manually?
end
def registered_manually?
encrypted_password.present?
end
end
And my profile.rb looks like
class Profile < ActiveRecord::Base
extend FriendlyId
friendly_id user.name, use: :slugged
belongs_to :user
accepts_nested_attributes_for :user
validates :website, allow_blank: true, uri: true
def website=(url_str)
if url_str.present?
url_str = "http://#{url_str}" unless url_str[/^https?/]
write_attribute :website, url_str
end
end
end
I think Problem is here:
Request parameters
{"action"=>"new", "controller"=>"users/registrations"}
Please suggest possible solution and explanation.
And users/registration:
class Users::RegistrationsController < Devise::RegistrationsController
layout 'land'
def create
params[:user][:profile_attributes].delete(:place)
end
protected
def after_sign_up_path_for(resource)
welcome_path
end
end
I am creating user in profile controller
def load_profile
#profile = Profile.friendly.find(params[:id])
if !#profile || #profile.user.blocked_users.include?(current_user)
redirect_to home_path
else
#user = #profile.user
end
end
#Rodrigo helped me find out error that error is due to Friendly_id can't create link with user instance.
There is an error on this line:
friendly_id user.name, use: :slugged
The variable user doesn't exists at Profile class scope. You should use something like this:
friendly_id :user_name, use: :slugged
def user_name
user.name
end
extend FriendlyId
friendly_id u_name, use: :slugged
def u_name
user.name
end
belongs_to :user
Have you defined user? what is user.name?

How to show errors for relations on the main Model

I want to include errors from a rather deep assocation in a parent:
class Order < ActiveRecord::Base
has_many :line_items
end
class LineItem < ActiveRecord::Base
belongs_to :product, polymorphic: true
end
class Site < ActiveRecord::Base
has_one :line_item, as: :product, autosave: true
validates :domain, presence: true
end
Used as:
product = Site.new(domain: nil)
order = Order.new
order.line_items << LineItem.new(product: product)
order.valid? #=> false
product.valid? #=> false
product.errors? #=> { 'domain' => 'cannot be blank' }
Is there some rails way, or association-parameter to make the errors
bubble up so that I get:
order.errors #=> { 'domain' => 'cannot be blank' }
In other words, that the Order, the top of the association,
transparently proxies the validation errors from its children?
I am aware of using simple before_validation hooks, like so:
class Order < ActiveRecord::Base
before_validation :add_errors_from_line_items
private
def add_errors_from_line_items
self.line_items.each do |line_item|
line_item.product.errors.each do |field, message|
errors.add(field, message)
end unless line_item.product.valid?
end
end
end
end
But I am wondering if there is not some ActiveRecord feature that I am overlooking.

Using Roles for Validations in Rails

Is it possible to use the roles used for attr_accessible and attr_protected? I'm trying to setup a validation that only executes when not an admin (like this sort of http://launchware.com/articles/whats-new-in-edge-scoped-mass-assignment-in-rails-3-1). For example:
class User < ActiveRecord::Base
def validate(record)
unless # role.admin?
record.errors[:name] << 'Wrong length' if ...
end
end
end
user = User.create({ ... }, role: "admin")
After looking into this and digging through the source code, it appears that the role passed in when creating an Active Record object is exposed through a protected method mass_assignment_role. Thus, the code in question can be re-written as:
class User < ActiveRecord::Base
def validate(record)
unless mass_assignment_role.eql? :admin
record.errors[:name] << 'Wrong length' if ...
end
end
end
user = User.create({ ... }, role: "admin")
Sure can would be something like this:
class User < ActiveRecord::Base
attr_accessible :role
validates :record_validation
def record_validation
unless self.role == "admin"
errors.add(:name, "error message") if ..
end
end
You could do this
class User < ActiveRecord::Base
with_options :if => :is_admin? do |admin|
admin.validates :password, :length => { :minimum => 10 } #sample validations
admin.validates :email, :presence => true #sample validations
end
end
5.4 Grouping conditional validations

Resources