I'm working on a Ruby on Rails project made by someone else. I'm tasked with allowing tables to be sorted by columns.
This specific table has columns that are from a related table. The table lists transaction history and shows the address (and other columns) of a property.
To access the value of the relation, the table body is using transaction.property.street_address
When a table column link is clicked, it sends the query params to the URL. Since transaction.street_address doesn't exist, I get a 500 error.
The street_address column is on the related property table, so I'm not sure how I can sort by that field
<div class="table-container">
<table class="table text text--body-3 text--medium">
<tr class="table__heading text--body-4">
<% if !current_user.user? %>
<th>Date</th>
<% end %>
<th><%= link_to 'Address', { order: 'street_address', desc: #desc == :asc} %></th>
<th>Property type</th>
<th><%= link_to 'Price', { order: 'sales_price', desc: #desc == :asc} %></th>
<% if !current_user.agent? %>
<th><%= link_to 'Agent', { order: '', desc: #desc == :asc} %></th>
<% end %>
<th>Type</th>
<th></th>
</tr>
<% #transactions.each do |transaction| %>
<tr class="table__row" data-link-to="<%= property_path(transaction.property) %>">
<td><%= transaction.sales_date.try(:strftime, '%B %d, %Y') %></td>
<td class="table__address"><%= transaction.property.street_address %></td>
<td><%= transaction.property.types.map(&:humanize).join(', ') %></td>
<td><%= number_to_currency(transaction.sales_price) %></td>
<% if !current_user.agent? %>
<td><span><%= transaction.user.full_name %></span></td>
<% end %>
<% if transaction.property.for_sale? %>
<td>Sale</td>
<% else %>
<td>Lease</td>
<% end %>
<td class="table__more">
<%= link_to "See the property", property_path(transaction.property), class: 'table__row__details button button--tertiary button--tertiary-small' %>
</td>
<td>
</tr>
<% end %>
</table>
</div>
Here is the Transaction Model:
class Transaction < ApplicationRecord
belongs_to :user
belongs_to :property
validates :cap_rate, presence: true
validates :sales_date, presence: true
validates :sales_price, presence: true
validates :noi, presence: true
scope :sold_property, -> do
joins(:property).where(properties: { for_sale: true })
end
scope :sold_before_by_broker_agents, -> (month, broker) do
joins(:user).where('sales_date < ?', month.end_of_month)
.merge(User.all_by_parent_id(broker.id))
end
end
Here is the Properties Model:
class Property < ApplicationRecord
extend FriendlyId
friendly_id :slug_text, use: :slugged
PHOTOS_LIMIT = 15
STATES_MAP = {
'Alabama' => 'AL', 'Alaska' => 'AK', 'Arizona' => 'AZ', 'Arkansas' => 'AR',
'California' => 'CA', 'Colorado' => 'CO', 'Connecticut' => 'CT',
'Delaware' => 'DE', 'District of Columbia' => 'DC', 'Florida' => 'FL',
'Georgia' => 'GA', 'Hawaii' => 'HI', 'Idaho' => 'ID', 'Illinois' => 'IL',
'Indiana' => 'IN', 'Iowa' => 'IA', 'Kansas' => 'KS', 'Kentucky' => 'KY',
'Louisiana' => 'LA','Maine' => 'ME', 'Maryland' => 'MD', 'Massachusetts' => 'MA',
'Michigan' => 'MI', 'Minnesota' => 'MN', 'Mississippi' => 'MS', 'Missouri' => 'MO',
'Montana' => 'MT', 'Nebraska' => 'NE', 'Nevada' => 'NV', 'New Hampshire' => 'NH',
'New Jersey' => 'NJ', 'New Mexico' => 'NM', 'New York' => 'NY', 'North Carolina' => 'NC',
'North Dakota' => 'ND', 'Ohio' => 'OH', 'Oklahoma' => 'OK', 'Oregon' => 'OR',
'Pennsylvania' => 'PA', 'Rhode Island' => 'RI', 'South Carolina' => 'SC',
'South Dakota' => 'SD', 'Tennessee' => 'TN', 'Texas' => 'TX', 'Utah' => 'UT',
'Vermont' => 'VT', 'Virginia' => 'VA', 'Washington' => 'WA', 'West Virginia' => 'WV',
'Wisconsin' => 'WI', 'Wyoming' => 'WY'
}.freeze
PROPERTY_TYPES = [:industrial, :retail, :shopping_center, :multifamily,
:specialty, :office, :health_care, :hostpitality_or_hotel,
:sports_and_entertainment, :land, :general_business, :condominium,
:residential_income].freeze
belongs_to :user
has_many :viewed_properties
has_many :last_viewed_by, through: :viewed_properties, source: :user
has_many :favorite_users_properties
has_many :favorited_by, through: :favorite_users_properties, source: :user
has_one :property_transaction, class_name: 'Transaction', dependent: :destroy
has_many :property_amenities, dependent: :destroy
has_many :amenities, through: :property_amenities
has_many :property_highlights, inverse_of: :property, dependent: :destroy
has_many :unit_prices, inverse_of: :property, dependent: :destroy
has_many :conversations, dependent: :destroy
has_many :property_photos, inverse_of: :property, dependent: :destroy
has_many :brochures, inverse_of: :property, dependent: :destroy
enum land_location: [:industrial_park, :urban]
enum rail: [:yes, :no]
accepts_nested_attributes_for :property_highlights, reject_if: :all_blank, allow_destroy: true
accepts_nested_attributes_for :unit_prices, reject_if: :all_blank, allow_destroy: true
accepts_nested_attributes_for :property_photos, reject_if: :all_blank, allow_destroy: true
accepts_nested_attributes_for :brochures, reject_if: :all_blank, allow_destroy: true
validate :listing_limit_exceeded, on: :create
validates :property_type,
:state,
:county,
:municipality,
:city,
:zip_code,
:street_address,
:listed_at,
:expires_at,
:property_status,
:description,
:status_date_change,
:parking,
:lot_size_units,
presence: true
validates :price,
:building_square_feet,
:lot_size,
:units,
:total_taxes,
numericality: { greater_than_or_equal_to: 0 }
validates :year_built, inclusion: { in: 1900..(Date.today.year + 5) }
validates :for_sale, presence: true, unless: :for_lease?
validates :for_lease, presence: true, unless: :for_sale?
validates :ceiling_height, numericality: { greater_than_or_equal_to: 0 }, if: -> { industrial? || office? }
validates :available_square_feet, numericality: { greater_than_or_equal_to: 0 }, unless: -> { multifamily? || specialty? || residential_income? || land?}
validates :max_contiguous_square_feet, numericality: { greater_than_or_equal_to: 0 }, if: -> { industrial? || retail? || office? }
validates :amps_volts_phase,
:number_of_dock_doors,
:number_of_drive_in_doors,
:available_office_square_feet,
numericality: { greater_than_or_equal_to: 0 }, if: :industrial?
validates :rail, presence: true, if: :industrial?
validates :income, :expense, numericality: { greater_than_or_equal_to: 0 }, if: -> { multifamily? || residential_income? }
validates :office_class, presence: true, if: :office?
validates :land_location, presence: true, if: :land?
validates :condominium_units,
numericality: { greater_than_or_equal_to: 0 }, if: :condominium?
validates :suite_number, presence: true, if: :condominium?
validates :flat_fee,
:percentage_of_sale,
#:referral_fee,
allow_blank: true, numericality: { greater_than_or_equal_to: 0 }
validates :apn, :zoning, presence: true, if: :for_sale?
validates :terms, presence: true, if: :for_lease?
geocoded_by :address
after_validation :geocode, if: :address_changed?
mount_uploader :cover, CoverUploader
mount_uploaders :photos, PhotoUploader
scope :last_viewed_properties, -> (user_id, limit) do
joins(:viewed_properties)
.merge(ViewedProperty.order(created_at: :desc).where("viewed_properties.user_id = ?", user_id).limit(limit))
end
default_scope { where(:deleted_at => nil) }
before_validation :set_expires_at
scope :not_sold, -> (parent_id: nil) do
if parent_id.present?
left_joins(:property_transaction)
.where(users: {parent_id: parent_id})
.where('transactions.property_id': nil)
else
left_joins(:property_transaction)
.where('transactions.property_id': nil)
end
end
scope :active, -> { where('expires_at >= ?', Time.now) }
scope :expiring_soon, -> { active.where('expires_at <= ?', 7.days.from_now) }
def self.property_types
PROPERTY_TYPES.each_with_index.map { |k, v| [k.to_s, v] }.to_h
end
def photos
property_photos.map(&:image)
end
PROPERTY_TYPES.each do |type|
define_method("#{type}?") do
types.include?(type.to_s)
end
define_method("exclusive_#{type}?") do
types.include?(type.to_s) && types.size == 1
end
end
def property_type
types[0]
end
def property_type=(value)
types[0] = value
end
def slug_text
[street_address, city, zip_code].compact.join(', ')
end
def state_short
STATES_MAP[state] || state
end
def address
['United States', state, county, zip_code, city, street_address].compact.join(', ')
end
def address_changed?
city_changed? || street_address_changed? || state_changed? || county_changed? || zip_code_changed?
end
def property_cover
return self.cover if cover.url
'example/property1.jpeg'
end
def secret_renewal_token
Digest::SHA256.hexdigest("#{user.authenticatable_salt}#{id}#{expires_at}")
end
def listed_at
self[:listed_at] || Date.today
end
private
def listing_limit_exceeded
if user.parent_id.present?
broker_user = User.find(user.parent_id)
current_subscription = broker_user.subscriptions.active.first
current_listing = Property.left_joins(:property_transaction, :user)
.merge(User.all_by_parent_id(user.parent_id))
.where('transactions.property_id': nil)
.count
else
current_listing = Property.left_joins(:property_transaction, :user).where(user_id: user.id).where('transactions.property_id': nil).count
current_subscription = user.subscriptions.active.first
end
unless current_subscription
errors.add(:base, "Your subscription has expired")
return
end
listing_limit = current_subscription.plan.listing_limit
if listing_limit <= current_listing
errors.add(:base, "Your listing limit is exceeded (current limit is: #{listing_limit})")
end
end
def set_expires_at
if listed_at
self.expires_at ||= (listed_at + 30.days)
end
end
end
making some assumptions here about your models...
in the transaction model:
class Transaction < ActiveRecord::Base
delegate :street_address, to: property
...etc
end
I've got a form view of an Order model (orders_form.html.erb) with a select option:
<%= f.select :pay_type, PaymentType.array_of_payment_types,
:prompt => 'Select a payment method' %>
PaymentType is another model and .array_of_payment_types is an array created out of the entries in the payment_type_name column, like so:
def self.array_of_payment_types
#array_of_payment_types ||= PaymentType.pluck(:pay_type_name)
end
... from models\payment_type.rb
But I get a proc 'empty?' error:
undefined method `empty?' for #
I hope my problem is clear, it seems like there is an obvious solution but I haven't found one reading other questions so far...
I will update with the relationships in the models...
My models:
payment_type.rb:
class PaymentType < ActiveRecord::Base
attr_accessible :pay_type_name
has_many :orders
validates :pay_type_name, :uniqueness
def self.names
all.collect { |pt| pt.pay_type_name }
end
def self.array_of_payment_types
PaymentType.all.map{ |p| [p.pay_type_name, p.id] }
end
end
order.rb:
class Order < ActiveRecord::Base
attr_accessible :address, :email, :name, :pay_type, :payment_type_id, :cart_id,
:product_id
has_many :line_items, :dependent => :destroy
belongs_to :payment_type
#PAYMENT_TYPES = ['Check','Purchase order','Credit card']
validates :name, :address, :email, :presence => true
validates :pay_type,
:presence => true,
:inclusion => { :in => proc { PaymentType.array_of_payment_types } }
def add_line_items_from_cart(cart)
cart.line_items.each do |item|
item.cart_id = nil
line_items << item
end
end
end
Try using the options_for_select:
# in the view:
<%= f.select :pay_type, options_for_select(PaymentType.array_of_payment_types),
:prompt => 'Select a payment method' %>
# in the PaymentType model:
def self.array_of_payment_types
PaymentType.all.map{ |p| [p.pay_type_name, p.id] }
end
You also need to update your validates statement in the Order model:
validates :pay_type,
:presence => true,
:inclusion => { :in => proc { PaymentType.pluck(:pay_type_name) } }
How can I return errors messages from a cross reference table with multiple records when I trying to create those? I'm trying this:
## activity_set.rb
class ActivitySet < ActiveRecord::Base
has_many :activity_set_lessons
has_many :lessons, :through => :activity_set_lessons
validates :name, :presence => true
def activity_set_lessons=(data)
data.each_with_index do |v, i|
activity_set_lessons.build(
:lesson_id => v[:lesson_id],
:sort_order => i,
:weight_percentage => v[:weight_percentage]
)
end
end
end
## activity_set_lesson.rb
class ActivitySetLesson < ActiveRecord::Base
belongs_to :activity_set
belongs_to :lesson
validates :lesson_id, :presence => true
validates_each :weight_percentage do |record, attr, value|
record.errors.add :base, "woot" if value.blank?
end
end
This is the request data:
## params[:activity_set]
"activity_set" => {
"name" => "hshshshs",
"keywords" => "",
"activity_set_lessons" => [
{"weight_percentage" => "", "lesson_id"=>"4"},
{"weight_percentage" => "", "lesson_id"=>"5"}
]
}
Error messages from #activity_set when I do #save:
{
"errors":{
"activity_set_lessons":["is invalid","is invalid"]
},
"full_messages":[
"Activity set lessons is invalid","Activity set lessons is invalid"
]
}
I always got the same error message even if I'm adding a custom one in the join table. How can I return a message like: "woot 1 is wrong" or something like that, per validation?.
Thanks.
make use of accepts_nested_attributes_for
## activity_set.rb
class ActivitySet < ActiveRecord::Base
has_many :activity_set_lessons
has_many :lessons, :through => :activity_set_lessons
validates :name, :presence => true
accepts_nested_attributes_for :activity_set_lessons
end
view will look like
= form_for #activity_set do |f|
[activity_set form fields ]
= f.fields_for :activity_set_lessons do |p|
= p.select :lession_id
= p.select :weight_percentage
I have a Location model with fields:
Location(id: integer, name: string, address: string, latitude: decimal, longitude: decimal, created_at: datetime, updated_at: datetime, rating: decimal, rating_count: integer)
In my location model, i have the following validations (which work fine):
attr_accessible :name, :address, :latitude, :longitude, :rating, :rating_count
validates :name, :presence => true, :length => { :maximum => 50 }
validates :address, :presence => true
validates :rating, :inclusion => 0..5
validates :rating, :presence => { :message => " cannot be blank" }
I also have a Post model with fields:
Post(id: integer, user_id: integer, location_id: integer, info: string, created_at: datetime, updated_at: date time)
And the following validations:
attr_accessible :info, :address, :name, :rating
belongs_to :user
belongs_to :location
attr_accessor :rating, :address, :name #Virtual attributes for the Post create form
validates :name, :presence => true, :length => { :maximum => 50 }
validates :address, :presence => true
validates :rating, :inclusion => 0..5
validates :rating, :presence => { :message => " cannot be blank" }
Now, the issue is that when I try to create a new Post, the validations for name and address work fine (just as they work for Location), but the validation for rating always fails. Even when I enter a rating of '3', I get the error Rating is not included in the list for some reason, although a rating of '3' is validated correctly if used directly with the Location#create action.
Does anyone know why only the rating validation isn't working as expected with Post, even though it works fine with Location, and the other validations behave identically with the two models ?
EDIT: Here's the create action from posts_controller:
def create
#post = current_user.posts.build(params[:post])
#location = Location.find_by_address_and_name(params[:post][:address], params[:post][:name])
if #location.nil?
#location = Location.find_by_address(params[:post][:address])
end
#If the address and the name of the location is the same, just update the rating for that location, and associate the post with the location
if (#location && params[:post][:name] == #location.name)
#post.location_id = #location.id
#location.rating_count += 1
#location.rating = ( (#location.rating + params[:post][:rating].to_r.to_d )/ #location.rating_count )
else
#post.location_id = Location.create(:address => params[:post][:address], :name => params[:post][:name], :rating => params[:post][:rating].to_r.to_d).id
end
#post.save
respond_to do |format|
if #post.save
#location.save! #Update the location only if the post was successful
format.html { redirect_to(#post, :notice => 'Post was successfully created.') }
format.xml { render :xml => #post, :status => :created, :location => #post }
else
format.html { render :action => "new" }
format.xml { render :xml => #post.errors, :status => :unprocessable_entity }
end
end
end
I know this is a LARGE post, but the main point is, 2 identical validations are failing in one place and succeeding in another. One of them validates attributes which are part of the database, the other validates virtual attributes, and I suspect this is the underlying problem.
validates :rating, :inclusion => ['0', '1', '2', '3', '4', '5']
# OR
validates :rating, :inclusion => (0..5).map(&:to_s)
UPD
add this method
def rating=param
#rating = param.to_f
end
so now you can use your original validation
validates :rating, :inclusion => (0..5)
You need to store an integer value 3 in rating, not the string "3".
Something like:
model.rating = params[:rating].to_i
What am I doing wrong here? The forms work but keep getting "undefined method `to_i' for :street1:Symbol" when trying to seed data.
EDIT = If I do everything as a singular address (has_one instead of has_many) seed works.
EDIT 2 = See answer below for others...
address.rb
class Address < ActiveRecord::Base
attr_accessible :street1, :street2, :city, :state, :zipcode, :deleted_at, :addressable_type, :addressable_id, :current, :full_address, :address_type
belongs_to :addressable, :polymorphic => true
scope :vendor, where("address_type='Vendor'")
before_save :update_full_address
def update_full_address
unless self.street2.blank?
street = self.street1 + "<br />" + self.street2 + "<br />"
else
street = self.street1 + "<br />"
end
citystatezip = self.city + ", " + self.state + " " + self.zipcode
self.full_address = street + citystatezip
end
end
vendor.rb
class Vendor < ActiveRecord::Base
attr_accessible :name, :contact, :phone, :addresses_attributes
has_many :addresses, :as => :addressable
accepts_nested_attributes_for :addresses, :allow_destroy => true, :reject_if => proc { |obj| obj.blank? }
end
seed data
require 'faker'
Vendor.delete_all
["Company A", "Company B", "Company C", "Company D"].each do |c|
params = {:vendor =>
{
:name => c,
:contact => Faker::Name.name,
:phone => Faker::PhoneNumber.phone_number,
:addresses_attributes => {
:street1 => Faker::Address.street_address,
:city => Faker::Address.city,
:state => Faker::Address.us_state_abbr,
:zipcode => Faker::Address.zip_code,
:address_type => "Vendor"
}
}
}
Vendor.create!(params[:vendor])
end
Note the [] for an array when dealing with has_many.
require 'faker'
Vendor.delete_all
["Company A", "Company B", "Company C", "Company D"].each do |c|
params = {:vendor =>
{
:name => c,
:contact => Faker::Name.name,
:phone => Faker::PhoneNumber.phone_number,
:addresses_attributes => [{
:street1 => Faker::Address.street_address,
:city => Faker::Address.city,
:state => Faker::Address.us_state_abbr,
:zipcode => Faker::Address.zip_code,
:address_type => "Vendor"
}]
}
}
Vendor.create!(params[:vendor])
end
accepts_nested_attributes_for :foo is so that you can create forms which create associated records. When you're building things in code, there's no need to use this. You can create the associated records using the association names instead of "address_attributes". Here's one way of doing it, but Rails does expose a bunch of ways of doing this same thing...
["Company A", "Company B", "Company C", "Company D"].each do |c|
vendor_address = Address.new :street1 => Faker::Address.street_address,
:city => Faker::Address.city,
:state => Faker::Address.us_state_abbr,
:zipcode => Faker::Address.zip_code,
:address_type => "Vendor"
Vendor.create! :name => c,
:contact => Faker::Name.name,
:phone => Faker::PhoneNumber.phone_number,
:addresses => [vendor_address]
end
If you are wanting to try and use the nested attributes way, then you don't need the :vendor => {} part of the hash, you can go straight into the params, and you need addresses_attributes to be an array, not a hash.