NoMethodError working with two models in Rails 4 - ruby-on-rails

I have a rating system for real estate agents. I have an agent model and an agent_review model. The rating is stored in the agent_review table, but I need to display the average rating in a view under the agent model and am running into some issue. All code is posted below, please and thank you in advance.
agent model:
has_many :agent_reviews
agent_review model:
belongs_to :agent
agent view:
<h3>Agent Rating: <%= #agent.agent_reviews.rating %> (<%= #agent.agent_reviews.count %>)</h3>
agent controller show method:
def show
#agent_reviews = AgentReview.all
#agent = Agent.find_by_slug(params[:id]) || Agent.find(params[:id])
if #agent.private_profile? && !current_agent&.super_admin?
redirect_to root_path, notice: "That user has a private profile"
else
#favorite_listings = #agent.liked_listings.available.includes(:neighborhood)
#agent_listings = #agent.sales_agent_listings.available.visible
#mate_posts = #agent.liked_mates.order(:when)
respond_to do |format|
format.html
format.json { render json: #agent }
end
end
end
error:

Adding to Jhon Feltz answer, you can to do this in a short mode. Like this:
def average_rating
agent_reviews = self.agent_reviews
agent_reviews.any? ? (agent_reviews.map(&:rating).sum / agent_reviews.count) : nil
end

#agent.agent_reviews is an Active Record relationship - there is no 'rating' for that, since it's more than one agent_review object (the fact that it's plural should tell you that).
So if an agent has 6 reviews, with ratings that vary from 1 to 5, you want to show the average of those. You need to add the following to the agent.rb model file:
def average_rating
if self.agent_reviews.any?
sum = 0
self.agent_reviews.each do |agent_review|
sum += agent_review.rating
end
return sum/self.agent_reviews.count
else
return nil # agent has no reviews, don't divide by zero!
end
end
(that's more verbose than it needs to be, you could condense it with some SQL magic)
And reference that new method in your view:
<h3>Agent Rating: <%= #agent.average_rating %> (<%= #agent.agent_reviews.count %>)</h3>

Related

How to refactor this code to make it more readable and efficient?

I need help refactoring the code.I ve tried by best landed with the following code. Is there anything That I can do
class OrdersController < ApplicationController
before_action :get_cart
before_action :set_credit_details, only: [:create]
# process order
def create
#order = Order.new(order_params)
# Add items from cart to order's ordered_items association
#cart.ordered_items.each do |item|
#order.ordered_items << item
end
# Add shipping and tax to order total
#order.total = case params[:order][:shipping_method]
when 'ground'
(#order.taxed_total).round(2)
when 'two-day'
#order.taxed_total + (15.75).round(2)
when "overnight"
#order.taxed_total + (25).round(2)
end
# Process credit card
# Check if card is valid
if #credit_card.valid?
billing_address = {
name: "#{params[:billing_first_name]} # .
{params[:billing_last_name]}",
address1: params[:billing_address_line_1],
city: params[:billing_city], state: params[:billing_state],
country: 'US',zip: params[:billing_zip],
phone: params[:billing_phone]
}
options = { address: {}, billing_address: billing_address }
# Make the purchase through ActiveMerchant
charge_amount = (#order.total.to_f * 100).to_i
response = ActiveMerchant::Billing::AuthorizeNetGateway.new(
login: ENV["AUTHORIZE_LOGIN"],
password: ENV["AUTHORIZE_PASSWORD"]
).purchase(charge_amount, #credit_card, options)
unless response.success?
#order.errors.add(:error, "We couldn't process your credit
card")
end
else
#order.errors.add(:error, "Your credit card seems to be invalid")
flash[:error] = "There was a problem processing your order. Please try again."
render :new && return
end
#order.order_status = 'processed'
if #order.save
# get rid of cart
Cart.destroy(session[:cart_id])
# send order confirmation email
OrderMailer.order_confirmation(order_params[:billing_email], session[:order_id]).deliver
flash[:success] = "You successfully ordered!"
redirect_to confirmation_orders_path
else
flash[:error] = "There was a problem processing your order. Please try again."
render :new
end
end
private
def order_params
params.require(:order).permit!
end
def get_cart
#cart = Cart.find(session[:cart_id])
rescue ActiveRecord::RecordNotFound
end
def set_credit_details
# Get credit card object from ActiveMerchant
#credit_card = ActiveMerchant::Billing::CreditCard.new(
number: params[:card_info][:card_number],
month: params[:card_info][:card_expiration_month],
year: params[:card_info][:card_expiration_year],
verification_value: params[:card_info][:cvv],
first_name: params[:card_info][:card_first_name],
last_name: params[:card_info][:card_last_name],
type: get_card_type # Get the card type
)
end
def get_card_type
length, number = params[:card_info][:card_number].size, params[:card_info][:card_number]
case
when length == 15 && number =~ /^(34|37)/
"AMEX"
when length == 16 && number =~ /^6011/
"Discover"
when length == 16 && number =~ /^5[1-5]/
"MasterCard"
when (length == 13 || length == 16) && number =~ /^4/
"Visa"
else
"Unknown"
end
end
end
Products with a price attribute. We have shopping Carts that have many Products through the OrderedItems join table. An OrderedItem belongs_to a Cart and a Product. It has a quantity attribute to keep track of the number of products ordered.
The OrderedItem also belongs_to an Order
I wanted to know if it can be refactored further.
First of all you should move all that business logic from the controller into models and services (OrderProcessService, PaymentService). All the controller's private methods belong to a PaymentService.
Split the code into smaller methods.
If doing that on the model level some things that come into my mind when reading your code are the following:
#order.add_items_from_cart(#cart)
#order.add_shipping_and_tax(shipping_method)
Orders should be first saved (persisted in DB), then processed (purchased with changing their status).
#order.save might fail after a successful payment, so a client will lose the money and not get their order.
the purchasing is an important and critical process, so you should make sure everything is ready for it (the order is valid and saved)
a client should be able to purchase later or after the payment page is accidentally reloaded without filling the form again
normally when a payment is performed you should send an order ID to the payment system. The payment system will store the ID and you will always know which order the payment belongs to.
There are a lot of other things to consider. You have a lot of work to do.

How find the distance between two objects?

Im using geocode. The idea is our partners can post products with an address. When they do so it fetches the latitude and longitude. Now when our customers go to buy that product they have to enter in a delivery address to tell us where to deliver the product. However if they delivery address is not within 20 miles of the product they are not allowed to get the product delivered.
Im getting an error message saying this "undefined method `latitude' for nil:NilClass"
Like I said the product.longitude, product.latitude is already set when the users are trying to order.
Not sure if it's because the order.delivery_address(lat, long) is not submitted into the database yet and its trying to check the distance. Here my code below
So My question is how can is how can i find the distance between the product address and order address and I want to show a alert message to the user if the distance between the two is over 20 miles.
def create
product = Product.find(params[:product_id])
if current_user == product.user
flash[:alert] = "You cannot purchase your own property"
elsif current_user.stripe_id.blank? || current_user.phone_number.blank?
flash[:alert] = " Please update your payment method and verify phone number please"
return redirect_to payment_method_path
elsif Geocoder::Calculations.distance_between([product.latitude, product.longitude], [#order.latitude, #order.longitude]) < 20
flash[:alert] = "The delivery address you provided is outside the delivery zone. Please choose a different product."
else
quantity = order_params[:quantity].to_i
#order = current_user.orders.build(order_params)
#order.product = product
#order.price = product.price
#order.total = product.price * quantity + product.delivery_price
# #order.save
if #order.Waiting!
if product.Request?
flash[:notice] = "Request sent successfully... Sit back and relax while our licensed dispensary fulfil your order :)"
else
#order.Approved!
flash[:notice] = "Your order is being filled and it will delivered shortly:)"
end
else
flash[:alert] = "Our licensed dispensary cannot fulfil your order at this time :( "
end
end
redirect_to product
end
You set #order in the following line:
#order = current_user.orders.build(order_params)
But you try to call its longitude and latitude methods above this, before you even set #order variable. To simply fix this problem, you can move this line up, it can even be located at the beginning of create method, since it doesn't depend on product or anything like that:
def create
#order = current_user.orders.build(order_params)
# ...
end
Although, there are number of problems in your code, like method names starting with capital letters (you can do it, but you shouldn't, it's against the convention) or overall complexity of the method.
You should move the business logic to the model where it belongs.
So lets start by creating a validation for the product distance:
class Order < ApplicationRecord
validates :product_is_within_range,
if: -> { product.present? } # prevents nil errors
# our custom validation method
def product_is_within_range
errors.add(:base, "The delivery address you provided is outside the delivery zone. Please choose a different product.") if product_distance < 20
end
def product_distance
Geocoder::Calculations.distance_between(product.coordinates, self.coordinates)
end
end
Then move the calculation of the total into the model:
class Order < ApplicationRecord
before_validation :calculate_total!, if: ->{ product && total.nil? }
def calculate_total!
self.total = product.price * self.quantity + product.delivery_price
end
end
But then you still have to deal with the fact that the controller is very broken. For example:
if current_user == product.user
flash[:alert] = "You cannot purchase your own property"
Should cause the method to bail. You´re not actually saving the record either. I would start over. Write failing tests for the different possible conditions (invalid parameters, valid parameters, user is owner etc) then write your controller code. Make sure you test each and every code branch.

Ruby on Rails - Undefined methods for NilClass

I'm creating a picture-rating app where users can click on pictures and rate them on a scale from 1 to 5. I'm trying to calculate the average rating of a picture. Before when users clicked on a rating value, that value became the picture's rating.
Rating: 5
If a user clicked on 1, the rating would change to 1
Rating: 1
When reality, the rating should have been 3.
(5 + 1) / 2
=> 3
Here's what I've accomplished so far in implementing this feature.
I added a migration to create two new columns for my Pictures Table
rails g migration AddRatingsToPictures ratings_count: integer, rating_total: integer
Both the new attributes, ratings_count and rating_total are integer types, meaning they are assigned a nil value at default.
p = Picture.first
p.attribute_names
=> ['id', 'title', 'category', 'stars', 'updated_at', 'created_at',
'ratings_count', 'rating_total']
p.ratings_count
=> nil
p.rating_total
=> nil
My only problem is the NilClass Error.
Here is my update method in my PicturesController.
def update
#picture = Picture.find(params[:id])
#picture.ratings_count = 0 if #picture.stars.nil?
#picture.rating_total = #picture.stars
#picture.rating_total += #picture.stars if #picture.stars_changed?
#picture.ratings_count += 1 if #picture.rating_total_changed?
if #picture.update_attributes(picture_params)
unless current_user.pictures.include?(#picture)
#picture = Picture.find(params[:id])
current_user.pictures << #picture
redirect_to #picture, :flash => { :success => "Thank you! This picture has been added to your Favorites List" }
else
redirect_to :action => 'index'
flash[:success] = 'Thank you! This picture has been updated'
end
else
render 'edit'
end
end
Here is my picture_param method in my PicturesController
def picture_params
params.require(:picture).permit(:title, :category, :genre, :stars)
end
Here is what the two new columns do
ratings_count: Calculates the number of times a picture has been rated
rating_total: Calculates the sum of the stars a picture has received
In the above code, I first set the ratings_count to 0 if the picture doesn't have a rating. This means that the picture hasn't been rated yet.
I then need to initially set the rating_total to the number of stars a picture has. If a user changed the star rating, I would add those stars to the rating_total. And if the total increased, that's my cue to increase the number of ratings.
Obviously, to calculate the average, I'd do something like this.
(#picture.rating_total / #picture.ratings_count).to_f
Now, I think I have the right idea but I know why this doesn't work. When columns are created with an integer value, by default they are set to nil. This leads to a NilClass Error when I load the web page.
undefined method `/' for nil:NilClass
Here is my code in the View
<li><strong>Rating:</strong> <%= pluralize((#picture.rating_total / #picture.ratings_count), 'Star') %></li>
Ok, the main reason it is not working is because
you fetch the picture
you check the stars from the database, and the NOT the passed form-parameters
you do update_attributes, which if I am not mistaken, used to set attributes and then save the complete object, but since rails 4 only updates the passed attributes (which is what you would expect)
One small remark: keeping the rating correct is a function I would place in the model, NOT in the controller.
Furthermore, how to handle the if nil, initialise to zero I wrote a short blogpost about. In short: overrule the getter.
So I would propose the following solution. In your model write
class Picture < ActiveRecord::Base
def ratings_count
self[:ratings_count] || 0
end
def ratings_total
self[:ratings_total] || 0
end
def add_rating(rating)
return if rating.nil? || rating == 0
self.ratings_count += 1
self.ratings_total += rating
self.stars = self.ratings_total.to_f / self.ratings_count
self.save
end
def rating
return 0 if self.ratings_count == 0
self.ratings_total.to_f / self.ratings_count
end
and then the code in your controller becomes much cleaner and readable:
def update
#picture = Picture.find(params[:id])
stars = picture_params.delete(:stars)
if #picture.update_attributes(picture_params)
#picture.add_rating stars
unless current_user.pictures.include?(#picture)
current_user.pictures << #picture
redirect_to #picture, :flash => { :success => "Thank you! This picture has been added to your Favorites List" }
else
redirect_to :action => 'index'
flash[:success] = 'Thank you! This picture has been updated'
end
else
render 'edit'
end
end
I first delete the :stars from the parameters, because I do not want to save those, I want to use those for the add_rating. I then try to update_attributes, which will fail if there are any failing validations, and if that is ok, I will add_rating which itself will handle nil or zero correctly. Well granted: I do not know how you handle a "non-rating" (nil? zero?). It is possible a rating of zero should be added, because it will add a rating, but most UI I know do not allow to select 0 as rating, so you might want to change the zero handling.
This will handle the case of uninitialized (nil) values in your attributes...
def update
#picture = Picture.find(params[:id])
if #picture.stars_changed?
#picture.ratings_count = (#picture.ratings_count || 0) + 1
#picture.rating_total = (#picture.rating_total || 0) + ( #picture.stars || 0)
end
You don't need an array of ratings or ratings persisted to database, assuming you only count votes where the rating changes, you can accumulate the count and the total and divide the two (which is, in fact, what you're doing so I'm preaching to the converted).
Although it seems to me that if I change a picture from 5 to 1 and it only changes to 3, I'm gonna keep clicking 1 :)
You could set the default value on the migration when you created it. But no worries, you can create a new migration to change it:
# Console
rails g migration change_default_for_ratings_count_and_rating_total
# Migration Code
class ChangeDefaultForRatingsCountAndRatingTotal < ActiveRecord::Migration
def change
change_column :pictures, :ratings_count, :integer, default: 0
change_column :pictures, :rating_total, :integer, default: 0
end
end
Keep in mind that some databases don't automatically assign newly updated default values to existing column entries, so maybe you will have to iterate over every picture already created with nil values and set to 0.
Ok, an alternative...
Do an after_initialize so the fields are never, never, ever nil. Even if you're creating a new Picture object, they'll be initialized as zero. Problem will go away.
class Picture << ActiveRecord::Base
after_initialize do |picture|
picture.ratings_count ||= 0
picture.rating_total ||= 0
end
...
end

Sorting in rails using helper

I'm a novice in ruby-on-rails.
I have an applications counting distance between metro station and a ATM.
There's two models with many-to-many relation: Station, Cashpoint. And there's a controller SHOW, that should get the station and show ATMs in order there's proximity to the station.
class StationsController < ApplicationController
def show
#station = Station.find(params[:id])
#cashpoints = #station.cashpoints.find(:all)
respond_to do |format|
format.html
end
end
end
Also there's a helper that counts distance using Google Directions API.
module StationsHelper
def count_distance(origin,destination)
...
return {:text => ... # 1 min
, :value => ... # 60 (seconds)
}
end
end
All this works properly.
But I'm wondering how to order ATMs by :value returned by StationsHelper?
I tried to write something in controller similar to:
#cashpoints = #station.cashpoints.find(:all,
:order => count_distance(#station.address, cashpoint.address)[:value])
But it's evidently doesn't work 'cause I have know idea how to link single cashpoint object
to count_distance method parameter.
May be you can help me, it appears that my project structure is wrong to do this.
Try this:
#cashpoints = #station.cashpoints.find(:all).sort_by { |c| count_distance(c.address, #station.address) }

Passing information from an existing record, to a new record, of a different table

If I have Table A with columns title and description, and I want to create a record in Table B, which also has a title and description column, is there a way to pass that information to new_b_path such that the /views/b/_form.html.erb populates with the data from the A record?
I am using the below code to clone a workout but this is acting within Table A so to speak. I want to clone across tables. Any ideas?
workouts_controller.rb
...
def new
#workout_count = Workout.count
if params[:cloned_workout]
workout_to_clone = Workout.find params[:cloned_workout]
#workout = workout_to_clone.clone
else
#workout = current_user.workouts.new
end
respond_to do |format|
format.html # new.html.erb
format.xml { render :xml => #workout }
end
end
...
/views/workouts/show.html.erb
<%= link_to "Clone Workout", new_workout_url + "?cloned_workout=#{#workout.id}", :class => "float-right" %>
Hmm. Judging by your explanation it sounds like a better strategy might be to simply create a table called Recommended with two belongs_to relationships that just has a workout_id and user_id column. Then you can associate the recommendation to a user and the workout. You could also have a gym_id or trainer_id column for the gym or trainer that made the recommendation. Make sense?

Resources