app/models/session.rb
require_dependency 'aircasting/username_param'
class Session < ApplicationRecord
self.skip_time_zone_conversion_for_attributes = %i[
start_time_local
end_time_local
]
include AirCasting::FilterRange
belongs_to :user
has_many :streams, inverse_of: :session, dependent: :destroy
has_many :measurements, through: :streams, inverse_of: :session
has_many :notes, inverse_of: :session, dependent: :destroy
validates :user, :uuid, :url_token, presence: true
validates :start_time_local, presence: true
validates :end_time_local, presence: true
validates :type, presence: :true
validates :url_token, :uuid, uniqueness: { case_sensitive: false }
accepts_nested_attributes_for :notes, :streams
before_validation :set_url_token, unless: :url_token
delegate :username, to: :user
acts_as_taggable
scope :local_minutes_range,
lambda { |minutes_from, minutes_to|
unless Utils.whole_day?(minutes_from, minutes_to)
field_in_minutes =
lambda { |field|
"(EXTRACT(HOUR FROM #{field}) * 60 + EXTRACT(MINUTE FROM #{
field
}))"
}
where "
(#{
field_in_minutes.call('start_time_local')
} BETWEEN :minutes_from AND :minutes_to)
OR
(#{
field_in_minutes.call('end_time_local')
} BETWEEN :minutes_from AND :minutes_to)
OR
(:minutes_from BETWEEN #{
field_in_minutes.call('start_time_local')
} AND #{field_in_minutes.call('end_time_local')})
",
minutes_from: minutes_from,
minutes_to: minutes_to
end
}
scope :mobile, -> { where('sessions.type = ?', 'MobileSession') }
scope :fixed, -> { where('sessions.type = ?', 'FixedSession') }
def self.filter_(data = {})
sessions =
order('sessions.start_time_local DESC')
.where(contribute: true)
.joins(:user)
tags = data[:tags].to_s.split(/[\s,]/)
sessions = sessions.tagged_with(tags, any: true) if tags.present?
usernames = AirCasting::UsernameParam.split(data[:usernames])
if usernames.present?
sessions = sessions.joins(:user).where(users: { username: usernames })
end
unless data[:is_indoor].nil?
sessions = sessions.where(is_indoor: data[:is_indoor])
end
unless data[:is_indoor] == true
if data[:east] && data[:west] && data[:north] && data[:south]
sessions = sessions.joins(:streams).merge(Stream.in_rectangle(data))
end
end
sensor_name = data[:sensor_name]
if sensor_name.present?
# this change in mysql->postgres affects performance, cause we need to compare lowercased strings
# we can create a column with lowercased sensor_name and use it for comparison so its faster
sessions = sessions.joins(:streams).where('LOWER(streams.sensor_name) IN (?)', Sensor.sensor_name(sensor_name))
end
unit_symbol = data[:unit_symbol]
if unit_symbol.present?
sessions =
sessions.joins(:streams).where(streams: { unit_symbol: unit_symbol })
end
if data[:time_from] && data[:time_to]
sessions =
filter_by_time_range(sessions, data[:time_from], data[:time_to])
end
sessions.joins(:streams).where('streams.measurements_count > 0')
end
def self.filter_sessions_ids_and_streams(data = {})
sessions = where('sessions.id IN (?)', data[:session_ids])
sensor_name = data[:sensor_name]
if sensor_name.present?
sessions =
sessions.joins(:streams).where(streams: { sensor_name: sensor_name })
end
unit_symbol = data[:unit_symbol]
if unit_symbol.present?
sessions =
sessions.joins(:streams).where(streams: { unit_symbol: unit_symbol })
end
sessions
end
def self.filter_by_time_range(sessions, time_from, time_to)
sessions
.where(
'(start_time_local BETWEEN :time_from AND :time_to)
OR
(end_time_local BETWEEN :time_from AND :time_to)
OR
(:time_from BETWEEN start_time_local AND end_time_local)',
time_from: time_from,
time_to: time_to,
)
.local_minutes_range(
Utils.minutes_of_day(time_from),
Utils.minutes_of_day(time_to),
)
end
def self.session_methods
%i[username streams]
end
def self.with_user_and_streams
includes(:user, streams: [:threshold_set])
end
def to_param
url_token
end
def west
direction('MIN', 'longitude')
end
def east
direction('MAX', 'longitude')
end
def north
direction('MAX', 'latitude')
end
def south
direction('MIN', 'latitude')
end
def as_json(opts = nil)
opts ||= {}
methods = opts[:methods] || %i[notes]
methods << :type
sensor_id = opts.delete(:sensor_id)
res = super(opts.merge(methods: methods))
map_of_streams = {}
strs = sensor_id ? streams.includes(:threshold_set).where(sensor_name: sensor_id) : streams.includes(:threshold_set).all
strs.each do |stream|
stream_json = stream.as_json
thresholds_json = stream.threshold_set.as_json(only: [:threshold_very_low, :threshold_low, :threshold_medium, :threshold_high, :threshold_very_high])
stream_json.merge!(thresholds_json)
if opts[:stream_measurements]
if type == 'FixedSession'
measurements_to_send =
get_measurement_scope(stream.id, opts[:last_measurement_sync])
map_of_streams[stream.sensor_name] =
stream_json.merge('measurements' => measurements_to_send)
else
map_of_streams[stream.sensor_name] =
stream_json.merge(
'measurements' => stream.measurements.as_json(
only: %i[time value latitude longitude]
)
)
end
else
map_of_streams[stream.sensor_name] = stream_json
end
end
res.merge!(
'streams' => map_of_streams,
'start_time' => self.start_time_local,
'end_time' => self.end_time_local,
)
res
end
def sync(session_data)
tag_list = session_data[:tag_list] || ''
session_data =
session_data.merge(tag_list: SessionBuilder.normalize_tags(tag_list))
transaction do
self.title = session_data[:title]
self.tag_list = session_data[:tag_list]
self.version += 1
self.save!
(session_data[:streams] || []).each do |key, stream_data|
if stream_data[:deleted]
streams
.where(
sensor_package_name: stream_data[:sensor_package_name],
sensor_name: stream_data[:sensor_name],
)
.each(&:destroy)
end
end
notes.where.not({ number: note_numbers(session_data) }).destroy_all
session_data[:notes].each do |note_data|
if note = notes.find_by_number(note_data[:number])
note.update(note_data)
else
note = Note.new(note_data)
note.session = self
note.save
end
end
end
end
def end_time_local=(time)
super(convert_time(time))
end
def start_time_local=(time)
super(convert_time(time))
end
def after_measurements_created; end
def fixed?
raise NotImplementedError, 'subclass did not define #fixed?'
end
private
def get_measurement_scope(stream_id, since_date)
if since_date
data = { stream_id: stream_id, since_date: since_date }
Measurement.unscoped.since(data).reverse.as_json
else
Measurement.unscoped.last_24_hours([stream_id]).reverse.as_json
end
end
def convert_time(time)
time = TimeToLocalInUTC.convert(time) if time.respond_to?(:strftime)
time
end
def direction(min_or_max, longitude_or_latitude)
measurements
.select("#{min_or_max}(#{longitude_or_latitude}) AS val")
.to_a
.first
.val
.to_f
end
def set_url_token
tg = TokenGenerator.new
token =
tg.generate_unique(5) do |token|
Session.where(url_token: token).count.zero?
end
self.url_token = token
end
after_destroy :insert_into_deleted_sessions
def insert_into_deleted_sessions
DeletedSession.where(uuid: uuid, user_id: user.id).first_or_create!
end
def note_numbers(session_data)
session_data[:notes].map { |note| note[:number] }
end
def generate_link(stream)
threshold_min = stream.threshold_set.threshold_very_low.to_i
threshold_low = stream.threshold_set.threshold_low.to_i
threshold_middle = stream.threshold_set.threshold_medium.to_i
threshold_high = stream.threshold_set.threshold_high.to_i
threshold_max = stream.threshold_set.threshold_very_high.to_i
session_id = stream.session_id
stream_id = stream.id
measurement_type = stream.measurement_type
sensor_name = stream.sensor_name
session_type =
case stream.session.type
when 'MobileSession'
'mobile'
when 'FixedSession'
'fixed'
end
encoded_params = {
sessionId: session_id,
streamId: stream_id,
thresholdMin: threshold_min,
thresholdLow: threshold_low,
thresholdMiddle: threshold_middle,
thresholdHigh: threshold_high,
thresholdMax: threshold_max,
currentUserSettings: 'MODAL_VIEW',
sessionType: session_type,
measurementType: CGI.escape(measurement_type),
sensorName: stream.sensor_name,
previousUserSettings: 'MAP_VIEW',
}
query_string = encoded_params.map { |k, v| "#{k}=#{v}" }.join("&")
"#{Rails.application.routes.url_helpers.root_path}?#{query_string}"
end
end