RGeo provides built in methods for POINT features, for example getter methods lat() and lon() to pull latitude and longitude values from a POINT object. Unfortunately, these don't work as setters. For example:
point = RGeo::Geographic.spherical_factory(:srid => 4326).point(3,5) // => #<RGeo::Geographic::SphericalPointImpl:0x817e521c "POINT (3.0 5.0)">
I can do this:
point.lat // => 5.0
point.lon // => 3.0
But I can't do:
point.lat = 4 // => NoMethodError: undefined method `lat=' for #<RGeo::Geographic::SphericalPointImpl:0x00000104024770>
Any suggestions as to how to implement setter methods? Would you do it in the Model or extend the Feature class?
I'm the author of RGeo, so you can consider this answer authoritative on that basis.
In short, PLEASE AVOID DOING THIS. RGeo objects intentionally have no setter methods because they are meant to be immutable objects. This is so that they can be cached, used as hash keys, used across threads, etc. Some of the RGeo calculations assume that the value of a feature object will never change, so making changes like this could have unexpected and unpredictable consequences.
If you really want a "changed" value, create a new object. For example:
p1 = my_create_a_point()
p2 = p1.factory.point(p1.lon + 20.0, p2.lat)
I have found something that works, although there might be more elegant solutions.
In my Location model I have added theses methods:
after_initialize :init
def init
self.latlon ||= Location.rgeo_factory_for_column(:latlon).point(0, 0)
end
def latitude
self.latlon.lat
end
def latitude=(value)
lon = self.latlon.lon
self.latlon = Location.rgeo_factory_for_column(:latlon).point(lon, value)
end
def longitude
self.latlon.lon
end
def longitude=(value)
lat = self.latlon.lat
self.latlon = Location.rgeo_factory_for_column(:latlon).point(value, lat)
end
I ended up doing something like this in my model:
class MyModel < ActiveRecord::Base
attr_accessor :longitude, :latitude
attr_accessible :longitude, :latitude
validates :longitude, numericality: { greater_than_or_equal_to: -180, less_than_or_equal_to: 180 }, allow_blank: true
validates :latitude, numericality: { greater_than_or_equal_to: -90, less_than_or_equal_to: 90 }, allow_blank: true
before_save :update_gps_location
def update_gps_location
if longitude.present? || latitude.present?
long = longitude || self.gps_location.longitude
lat = latitude || self.gps_location.latitude
self.gps_location = RGeo::Geographic.spherical_factory(srid: 4326).point(long, lat)
end
end
end
Then you can just update the position like so:
my_model.update_attributes(longitude: -122, latitude: 37)
I didn't load up longitude/latitude in an after_initialize block because in my app we never need to read the data, only write it. You could easily add that though.
Credit to this answer for the validations.
Related
We want to let our website's members list a second home, for those who spend the winter in one place and the summer in another. geocoder's documentation explains how to perform a query on a second set of coordinates, but it doesn't say how to set the second pair of coordinates in the database so we can search on them.
If we do this, for example:
geocoded_by :location, latitude: :latitude, longitude: :longitude
after_validation :geocode, if: ->(obj) { obj.location.present? && obj.location_changed? }
reverse_geocoded_by :latitude, :longitude, location: :location do |obj, results|
if (geo = results.first)
obj.city = geo.city
obj.state = geo.state
obj.country = geo.country
obj.state_code = geo.state_code
obj.country_code = geo.country_code
end
end
after_validation :reverse_geocode, if: ->(obj) { obj.location.present? && obj.location_changed? }
geocoded_by :location2, latitude: :latitude2, longitude2: :longitude2
after_validation :geocode, if: ->(obj) { obj.location2.present? && obj.location2_changed? }
reverse_geocoded_by :latitude2, :longitude2, location: :location2 do |obj, results|
if (geo = results.first)
obj.city2 = geo.city
obj.state2 = geo.state
obj.country2 = geo.country
obj.state_code2 = geo.state_code
obj.country_code2 = geo.country_code
end
end
after_validation :reverse_geocode, if: ->(obj) { obj.location2.present? && obj.location2_changed? }
It seems like the second geocoded_by overwrites the first instead of adding a second action. Can anyone suggest a way to do this?
Thanks very much!
Thank again for your help, #TimKretschmer; it's worked well for us. A couple of caveats for anyone following this path:
First, if you create a polymorphic association, Locations will be tied both to the models that use them and the specific instances of those models. This means that, at best, you can create (and index on) a single Location for each User + user_id, each Event + event_id, etc., etc. This is a little disappointing — I would have preferred a single Location for "Paris" that everything used. I doesn't even look like I can have a single Location for all Users; there's going to be a "Paris" location for User 3 and another "Paris" for user 437.
Still, it's working. I cleaned it up a bit by running the users' input through Geocoder and saving the results if we get a hit:
def create
location = Geocoder.address(params[:user][:location])
params[:user][:location] = location unless location.nil?
location2 = Geocoder.address(params[:user][:location2])
params[:user][:location2] = location2 unless location2.nil?
#user = User.new(user_params)
[etc.]
end
Most importantly, anyone doing this needs to be aware that the way Geocoder uses ActiveRecord means you can't use pluck with near. I ended up using map instead:
User.where(id: [Location
.where(locatable_type: 'User')
.near(#search_place, #search_distance)
.map(&:locatable_id)])
Best wishes,
Jeff
I am making a custom dashboard for a school application that requires me to calculate some KPIs, the way am doing it right now is calling several class methods from the Opportunity class in the dashboard/index action from the controller, and storing each method result in a variable that is going to be used in a tile. So each variable is a different tile of the dashboard.
The methods belong to the Opportunity class shown below:
class Opportunity < ApplicationRecord
belongs_to :organization
belongs_to :opportunity_status
has_many :tasks, dependent: :destroy
has_many :opportunity_status_logs, dependent: :destroy
before_create :create_status_log
after_update :create_status_log, if: :opportunity_status_id_changed?
validates :name, :description, :revenue, :opportunity_status_id, :closing_date, presence: true
validates :name, :description, format: { with: /\A[[:alpha:]a-zA-Z0-9ñÑ#()\-.,\s]+\z/ }
validates :revenue, numericality: true
validates :closing_date, inclusion: { in: (Time.zone.today..Time.zone.today+5.years) }
def create_status_log
OpportunityStatusLog.create(opportunity_id: self.id, opportunity_status_id: self.opportunity_status_id)
end
def status_updated_by(user)
#status_log = self.opportunity_status_logs.last
#status_log.user_id = user.id
#status_log.save!
end
def self.actives
self.where.not(opportunity_status_id: [11,12])
end
def self.won
self.where(opportunity_status_id: 11)
end
def self.lost
self.where(opportunity_status_id: 12)
end
def self.average_revenue
self.won.average(:revenue)
end
def self.minimum_revenue
self.won.minimum(:revenue)
end
def self.maximum_revenue
self.won.maximum(:revenue)
end
def self.filter_by_status(status_id)
self.where(opportunity_status: status_id)
end
def self.relative_percentage(item_amount, total)
item_amount * 100 / total
end
def self.conversion_rate
self.won.count / self.all.count.to_f * 100
end
def self.potential_revenue
self.actives.sum(:revenue)
end
end
and this is the way the controller is structured:
class DashboardController < ApplicationController
before_action :authenticate_user!
def index
#opportunities = Opportunity.includes(:opportunity_status).all
#actives = Opportunity.actives.count
#won = Opportunity.won.count
#lost = Opportunity.lost.count
#average_revenue = Opportunity.average_revenue
#minimum_revenue = Opportunity.minimum_revenue
#maximum_revenue = Opportunity.maximum_revenue
#in_appreciation = Opportunity.filter_by_status(6).count
#in_value_proposition = Opportunity.filter_by_status(7).count
#in_management_analysis = Opportunity.filter_by_status(8).count
#in_proposal = Opportunity.filter_by_status(9).count
#in_review = Opportunity.filter_by_status(10).count
#app_perc = Opportunity.relative_percentage(#in_appreciation, #opportunities.count)
#vp_perc = Opportunity.relative_percentage(#in_value_proposition, #opportunities.count)
#ma_perc = Opportunity.relative_percentage(#in_management_analysis, #opportunities.count)
#pp_perc = Opportunity.relative_percentage(#in_proposal, #opportunities.count)
#rw_perc = Opportunity.relative_percentage(#in_review, #opportunities.count)
#conversion_rate = '%.2f' % [Opportunity.conversion_rate]
#potential_revenue = Opportunity.potential_revenue
end
end
Even though it works as expected, it looks like the controller is a bit too fat and I feel that with the current approach if the app scales it will be very slow due to the amount of queries that are being done. So, is there a way to refactor this in order to optimize the data retrieval and the displaying of the KPIs?
Thanks in advance
You can try implementing Facade Pattern in Rails. It will make your controller skinny but on the query part you will still be needing to make those queries, there is no way to skip that.
You can try to optimize db by adding index and creating sql views in future when you get performance lag, at this time it will be like premature optimization
I'd like to convert a unix time to human time before saving my object from an api.
But I cannot access to my method format date, it raise me :
undefined method `format_date' for 1467738900000:Fixnum
My model :
class Conference < ActiveRecord::Base
validates_presence_of :title, :date
validates :date, :uniqueness => true
def self.save_conference_from_api
data = self.new.data_from_api
self.new.parisrb_conferences(data).each do |line|
conference = self.new
conference.title = line['name']
conference.date = line['time'].format_date
conference.url = line['link']
if conference.valid?
conference.save
end
end
self.all
end
def format_date
DateTime.strptime(self.to_s,'%Q')
end
line['time'] is not an instance of your Conference class, so you can't call format_date method on it. Instead, for example, you can make format_date a class method:
def self.format_date str
DateTime.strptime(str.to_s,'%Q')
end
And then call it like this:
conference.date = format_date(line['time'])
The other option is to use a before_validation callback (attribute assignment will be as follows: conference.date = line['time'] and there is no need for format_date method):
before_validation -> r { r.date = DateTime.strptime(r.date.to_s,'%Q') }
You are getting the date in unix time milliseconds. You can do like this
conference.date = DateTime.strptime(line['time'].to_s,'%Q')
I'm wanting to validate that my height attribute is within a bunch of different ranges. So my attempt was something like what I did below... however this is incorrect. How should this be done? Thanks!
validates :height, :numericality => { in: { 5020..5028, 5030..5038, 5040..5048, 5050..5058, 5060..5068, 5070..5078, 5080..5088, 5090..5098, 5100..5108, 5110..5118,
6000..6008, 6010..6018, 6020..6028, 6030..6038, 6040..6048, 6050..6058, 6060..6068, 6070..6078, 6080..6088, 6090..6098, 6100..6108, 6110..6118,
7000..7008, 7010..7018, 7020..7028, 7030..7038, 7040..7048, 7050..7058, 7060..7068, 7070..7078, 7080..7088, 7090..7098, 7100..7108, 7110..7118 } }
You can put that in a custom validate method:
class YourModel < ActiveRecord::Base
VALID_HEIGHT_RANGES = [5020..5028, 5030..5038, 5040..5048, 5050..5058, 5060..5068, 5070..5078, 5080..5088, 5090..5098, 5100..5108, 5110..5118, 6000..6008, 6010..6018, 6020..6028, 6030..6038, 6040..6048, 6050..6058, 6060..6068, 6070..6078, 6080..6088, 6090..6098, 6100..6108, 6110..6118, 7000..7008, 7010..7018, 7020..7028, 7030..7038, 7040..7048, 7050..7058, 7060..7068, 7070..7078, 7080..7088, 7090..7098, 7100..7108, 7110..7118]
validate :height_in_valid_range
private
def height_in_valid_range
VALID_HEIGHT_RANGES.each do |range|
unless range.include? height
errors.add :height, "not in valid range"
break
end
end
end
end
I have a validation method that has to verify values assigned in another method, how can i get it to recognise those values before validation? the pay_must_be_same_to_amount method needs some values from the create_items_from_readings method
class Invoice < ActiveRecord::Base
attr_accessible :approved_by, :due_date, :invoice_date, :reading_ids, :terms, :customer_id, :customer, :status, :reference_no, :payment_method, :amount, :payment_date
has_many :invoice_items, :dependent => :destroy
belongs_to :customer, :inverse_of => :invoices
validate :pay_must_be_same_to_amount
def create_item_from_readings
item = invoice_items.new
item.rate = customer.unit_cost
readings_in_this_period = customer.unbilled_readings.where('date_of_reading <= ?', invoice_date).order(:date_of_reading)
return nil if readings_in_this_period.empty?
self.reading_ids = readings_in_this_period.collect(&:id).join(',')
total_units = 0
readings_in_this_period.each do |reading|
total_units = total_units + reading.units_used1 + reading.units_used2 + reading.units_used3
end
item.amount = total_units * customer.unit_cost * customer.ct_ratio
item.tax_amount = (item.amount * Settings.vat) if customer.pays_vat
invoice_from_reading = readings_in_this_period.first.previous_reading
invoice_from_reading ||= readings_in_this_period.first
invoice_to_reading = readings_in_this_period.last
#Select Item description based on Phase type
if customer.phase_type == 'Single Phase'
item.description = "Electricity used from #{invoice_from_reading.date_of_reading.strftime('%d/%m/%Y')} with readings #{invoice_from_reading.reading1} to #{invoice_to_reading.date_of_reading.strftime('%d/%m/%Y')} with reading #{invoice_to_reading.reading1} - #{total_units.to_i} total units"
else
item.description = "Electricity used from #{invoice_from_reading.date_of_reading.strftime('%d/%m/%Y')} with readings, R1: #{invoice_from_reading.reading1}, R2: #{invoice_from_reading.reading2}, R3: #{invoice_from_reading.reading3} to #{invoice_to_reading.date_of_reading.strftime('%d/%m/%Y')} with readings, R1: #{invoice_to_reading.reading1}, R2:#{invoice_to_reading.reading2}, R3: # {invoice_to_reading.reading3}- #{total_units.to_i} total units"
end
end
end
and the validation method is below, it needs to compare the item.amount above to the amount in the class Invoice
def pay_must_be_same_to_amount
if item.amount < self.amount && item.amount != self.amount
self.errors.add :amount, 'The payment amount should be equal to amount on invoice'
end
end
end
A few comments: create_item_from_readings is way too complicated. I can't tell what it's supposed to do, but if you run it, I believe it will return a string (one of the two from the last if statement).
If all you need to do is compare item.amount to the invoice amount attribute, that's simple. You can use your validation method almost as you've written it, plus a few other methods as needed.
def item_amount
total_units * customer.unit_cost * customer.ct_ratio
end
def total_units
readings_in_this_period.inject(0) {|total,r| total + r.units_used1 + r.units_used2 + r.units_used3 }
end
def pay_must_be_same_to_amount
if item_amount != amount
errors.add :amount, 'The payment amount should be equal to amount on invoice'
end
end
The code for both of those supplementary methods is simply modified code from your longer method.
A good rule of practice is that if a method is longer than one line, and you can't tell what it's for by glancing at it, it's too long (this isn't always true, but it's worth considering for complicated methods).
The solution to the question is
def pay_must_be_same_to_amount
sum = 0
self.invoice_items.each do |invoice_item|
sum = sum + invoice_item.amount
end
if sum != self.amount
self.errors.add :amount, 'The payment amount should be equal to amount on invoice'
end
end