mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-09 16:57:12 -05:00
Clean up some mess
This commit is contained in:
parent
708bca26eb
commit
f969d5d3e6
22 changed files with 325 additions and 260 deletions
|
|
@ -31,7 +31,8 @@ class MapController < ApplicationController
|
|||
|
||||
def build_tracks
|
||||
track_ids = extract_track_ids
|
||||
TrackSerializer.new(current_user, track_ids).call
|
||||
|
||||
TracksSerializer.new(current_user, track_ids).call
|
||||
end
|
||||
|
||||
def calculate_distance
|
||||
|
|
|
|||
|
|
@ -6,20 +6,7 @@ class Tracks::CreateJob < ApplicationJob
|
|||
def perform(user_id, start_at: nil, end_at: nil, mode: :daily)
|
||||
user = User.find(user_id)
|
||||
|
||||
# Translate mode parameter to Generator mode
|
||||
generator_mode = case mode
|
||||
when :daily then :daily
|
||||
when :none then :incremental
|
||||
else :bulk
|
||||
end
|
||||
|
||||
# Generate tracks and get the count of tracks created
|
||||
tracks_created = Tracks::Generator.new(
|
||||
user,
|
||||
start_at: start_at,
|
||||
end_at: end_at,
|
||||
mode: generator_mode
|
||||
).call
|
||||
tracks_created = Tracks::Generator.new(user, start_at:, end_at:, mode:).call
|
||||
|
||||
create_success_notification(user, tracks_created)
|
||||
rescue StandardError => e
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
# track.distance # => 5000 (meters stored in DB)
|
||||
# track.distance_in_unit('km') # => 5.0 (converted to km)
|
||||
# track.distance_in_unit('mi') # => 3.11 (converted to miles)
|
||||
# track.formatted_distance('km') # => "5.0 km"
|
||||
#
|
||||
module DistanceConvertible
|
||||
extend ActiveSupport::Concern
|
||||
|
|
@ -38,21 +37,11 @@ module DistanceConvertible
|
|||
distance.to_f / conversion_factor
|
||||
end
|
||||
|
||||
def formatted_distance(unit, precision: 2)
|
||||
converted_distance = distance_in_unit(unit)
|
||||
"#{converted_distance.round(precision)} #{unit}"
|
||||
end
|
||||
|
||||
def distance_for_user(user)
|
||||
user_unit = user.safe_settings.distance_unit
|
||||
distance_in_unit(user_unit)
|
||||
end
|
||||
|
||||
def formatted_distance_for_user(user, precision: 2)
|
||||
user_unit = user.safe_settings.distance_unit
|
||||
formatted_distance(user_unit, precision: precision)
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def convert_distance(distance_meters, unit)
|
||||
return 0.0 unless distance_meters.present?
|
||||
|
|
@ -66,10 +55,5 @@ module DistanceConvertible
|
|||
|
||||
distance_meters.to_f / conversion_factor
|
||||
end
|
||||
|
||||
def format_distance(distance_meters, unit, precision: 2)
|
||||
converted = convert_distance(distance_meters, unit)
|
||||
"#{converted.round(precision)} #{unit}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class Point < ApplicationRecord
|
|||
after_create :set_country
|
||||
after_create_commit :broadcast_coordinates
|
||||
after_create_commit :trigger_incremental_track_generation, if: -> { import_id.nil? }
|
||||
after_commit :recalculate_track, on: :update
|
||||
after_commit :recalculate_track, on: :update, if: -> { track.present? }
|
||||
|
||||
def self.without_raw_data
|
||||
select(column_names - ['raw_data'])
|
||||
|
|
@ -99,8 +99,6 @@ class Point < ApplicationRecord
|
|||
end
|
||||
|
||||
def recalculate_track
|
||||
return unless track.present?
|
||||
|
||||
track.recalculate_path_and_distance!
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,38 +1,23 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TrackSerializer
|
||||
def initialize(user, track_ids)
|
||||
@user = user
|
||||
@track_ids = track_ids
|
||||
def initialize(track)
|
||||
@track = track
|
||||
end
|
||||
|
||||
def call
|
||||
return [] if track_ids.empty?
|
||||
|
||||
tracks = user.tracks
|
||||
.where(id: track_ids)
|
||||
.order(start_at: :asc)
|
||||
|
||||
tracks.map { |track| serialize_track_data(track) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :track_ids
|
||||
|
||||
def serialize_track_data(track)
|
||||
{
|
||||
id: track.id,
|
||||
start_at: track.start_at.iso8601,
|
||||
end_at: track.end_at.iso8601,
|
||||
distance: track.distance.to_i,
|
||||
avg_speed: track.avg_speed.to_f,
|
||||
duration: track.duration,
|
||||
elevation_gain: track.elevation_gain,
|
||||
elevation_loss: track.elevation_loss,
|
||||
elevation_max: track.elevation_max,
|
||||
elevation_min: track.elevation_min,
|
||||
original_path: track.original_path.to_s
|
||||
id: @track.id,
|
||||
start_at: @track.start_at.iso8601,
|
||||
end_at: @track.end_at.iso8601,
|
||||
distance: @track.distance.to_i,
|
||||
avg_speed: @track.avg_speed.to_f,
|
||||
duration: @track.duration,
|
||||
elevation_gain: @track.elevation_gain,
|
||||
elevation_loss: @track.elevation_loss,
|
||||
elevation_max: @track.elevation_max,
|
||||
elevation_min: @track.elevation_min,
|
||||
original_path: @track.original_path.to_s
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
22
app/serializers/tracks_serializer.rb
Normal file
22
app/serializers/tracks_serializer.rb
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class TracksSerializer
|
||||
def initialize(user, track_ids)
|
||||
@user = user
|
||||
@track_ids = track_ids
|
||||
end
|
||||
|
||||
def call
|
||||
return [] if track_ids.empty?
|
||||
|
||||
tracks = user.tracks
|
||||
.where(id: track_ids)
|
||||
.order(start_at: :asc)
|
||||
|
||||
tracks.map { |track| TrackSerializer.new(track).call }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :track_ids
|
||||
end
|
||||
|
|
@ -7,7 +7,7 @@ module Places
|
|||
end
|
||||
|
||||
def call
|
||||
geodata = Geocoder.search([@place.lat, @place.lon], units: :km, limit: 1, distance_sort: true).first
|
||||
geodata = Geocoder.search([place.lat, place.lon], units: :km, limit: 1, distance_sort: true).first
|
||||
|
||||
return if geodata.blank?
|
||||
|
||||
|
|
@ -15,21 +15,29 @@ module Places
|
|||
return if properties.blank?
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
@place.name = properties['name'] if properties['name'].present?
|
||||
@place.city = properties['city'] if properties['city'].present?
|
||||
@place.country = properties['country'] if properties['country'].present?
|
||||
@place.geodata = geodata.data if DawarichSettings.store_geodata?
|
||||
@place.save!
|
||||
update_place_name(properties, geodata)
|
||||
|
||||
if properties['name'].present?
|
||||
@place
|
||||
.visits
|
||||
.where(name: Place::DEFAULT_NAME)
|
||||
.update_all(name: properties['name'])
|
||||
end
|
||||
update_visits_name(properties) if properties['name'].present?
|
||||
|
||||
@place
|
||||
place
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :place
|
||||
|
||||
def update_place_name(properties, geodata)
|
||||
place.name = properties['name'] if properties['name'].present?
|
||||
place.city = properties['city'] if properties['city'].present?
|
||||
place.country = properties['country'] if properties['country'].present?
|
||||
place.geodata = geodata.data if DawarichSettings.store_geodata?
|
||||
|
||||
place.save!
|
||||
end
|
||||
|
||||
def update_visits_name(properties)
|
||||
place.visits.where(name: Place::DEFAULT_NAME).update_all(name: properties['name'])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -48,6 +48,7 @@ class Tracks::Generator
|
|||
Rails.logger.debug "Generator: created #{segments.size} segments"
|
||||
|
||||
tracks_created = 0
|
||||
|
||||
segments.each do |segment|
|
||||
track = create_track_from_segment(segment)
|
||||
tracks_created += 1 if track
|
||||
|
|
@ -146,10 +147,6 @@ class Tracks::Generator
|
|||
day.beginning_of_day.to_i..day.end_of_day.to_i
|
||||
end
|
||||
|
||||
def incremental_mode?
|
||||
mode == :incremental
|
||||
end
|
||||
|
||||
def clean_existing_tracks
|
||||
case mode
|
||||
when :bulk then clean_bulk_tracks
|
||||
|
|
|
|||
|
|
@ -36,12 +36,7 @@ class Tracks::IncrementalProcessor
|
|||
start_at = find_start_time
|
||||
end_at = find_end_time
|
||||
|
||||
Tracks::CreateJob.perform_later(
|
||||
user.id,
|
||||
start_at: start_at,
|
||||
end_at: end_at,
|
||||
mode: :none
|
||||
)
|
||||
Tracks::CreateJob.perform_later(user.id, start_at:, end_at:, mode: :incremental)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ module Tracks::Segmentation
|
|||
return true if time_diff_seconds > time_threshold_seconds
|
||||
|
||||
# Check distance threshold - convert km to meters to match frontend logic
|
||||
distance_km = calculate_distance_kilometers_between_points(previous_point, current_point)
|
||||
distance_km = calculate_km_distance_between_points(previous_point, current_point)
|
||||
distance_meters = distance_km * 1000 # Convert km to meters
|
||||
|
||||
return true if distance_meters > distance_threshold_meters
|
||||
|
|
@ -85,7 +85,7 @@ module Tracks::Segmentation
|
|||
false
|
||||
end
|
||||
|
||||
def calculate_distance_kilometers_between_points(point1, point2)
|
||||
def calculate_km_distance_between_points(point1, point2)
|
||||
lat1, lon1 = point_coordinates(point1)
|
||||
lat2, lon2 = point_coordinates(point2)
|
||||
|
||||
|
|
|
|||
|
|
@ -113,7 +113,6 @@ class Users::SafeSettings
|
|||
end
|
||||
|
||||
def distance_unit
|
||||
# km or mi
|
||||
settings.dig('maps', 'distance_unit')
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card bg-base-300 w-96 shadow-xl m-5">
|
||||
<div class="card bg-base-300 w-96 shadow-xl m-5">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">Visits suggestions</h2>
|
||||
<p>Enable or disable visits suggestions. It's a background task that runs every day at midnight. Disabling it might be useful if you don't want to receive visits suggestions or if you're using the Dawarich iOS app, which has its own visits suggestions.</p>
|
||||
|
|
|
|||
|
|
@ -5,11 +5,11 @@ SELF_HOSTED = ENV.fetch('SELF_HOSTED', 'true') == 'true'
|
|||
MIN_MINUTES_SPENT_IN_CITY = ENV.fetch('MIN_MINUTES_SPENT_IN_CITY', 60).to_i
|
||||
|
||||
DISTANCE_UNITS = {
|
||||
km: 1000, # to meters
|
||||
km: 1000, # to meters
|
||||
mi: 1609.34, # to meters
|
||||
m: 1, # already in meters
|
||||
ft: 0.3048, # to meters
|
||||
yd: 0.9144 # to meters
|
||||
m: 1, # already in meters
|
||||
ft: 0.3048, # to meters
|
||||
yd: 0.9144 # to meters
|
||||
}.freeze
|
||||
|
||||
APP_VERSION = File.read('.app_version').strip
|
||||
|
|
|
|||
|
|
@ -4,13 +4,13 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe AreaVisitsCalculationSchedulingJob, type: :job do
|
||||
describe '#perform' do
|
||||
let!(:user) { create(:user) }
|
||||
let!(:area) { create(:area, user: user) }
|
||||
let(:user) { create(:user) }
|
||||
let(:area) { create(:area, user: user) }
|
||||
|
||||
it 'calls the AreaVisitsCalculationService' do
|
||||
expect(AreaVisitsCalculatingJob).to receive(:perform_later).with(user.id)
|
||||
expect(AreaVisitsCalculatingJob).to receive(:perform_later).with(user.id).and_call_original
|
||||
|
||||
described_class.new.perform_now
|
||||
described_class.new.perform
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,12 +14,10 @@ RSpec.describe Tracks::CreateJob, type: :job do
|
|||
allow(generator_instance).to receive(:call)
|
||||
allow(Notifications::Create).to receive(:new).and_return(notification_service)
|
||||
allow(notification_service).to receive(:call)
|
||||
allow(generator_instance).to receive(:call).and_return(2)
|
||||
end
|
||||
|
||||
it 'calls the generator and creates a notification' do
|
||||
# Mock the generator to return the count of tracks created
|
||||
allow(generator_instance).to receive(:call).and_return(2)
|
||||
|
||||
described_class.new.perform(user.id)
|
||||
|
||||
expect(Tracks::Generator).to have_received(:new).with(
|
||||
|
|
@ -48,12 +46,10 @@ RSpec.describe Tracks::CreateJob, type: :job do
|
|||
allow(generator_instance).to receive(:call)
|
||||
allow(Notifications::Create).to receive(:new).and_return(notification_service)
|
||||
allow(notification_service).to receive(:call)
|
||||
allow(generator_instance).to receive(:call).and_return(1)
|
||||
end
|
||||
|
||||
it 'passes custom parameters to the generator' do
|
||||
# Mock generator to return the count of tracks created
|
||||
allow(generator_instance).to receive(:call).and_return(1)
|
||||
|
||||
described_class.new.perform(user.id, start_at: start_at, end_at: end_at, mode: mode)
|
||||
|
||||
expect(Tracks::Generator).to have_received(:new).with(
|
||||
|
|
@ -73,72 +69,6 @@ RSpec.describe Tracks::CreateJob, type: :job do
|
|||
end
|
||||
end
|
||||
|
||||
context 'with mode translation' do
|
||||
before do
|
||||
allow(Tracks::Generator).to receive(:new).and_return(generator_instance)
|
||||
allow(generator_instance).to receive(:call) # No tracks created for mode tests
|
||||
allow(Notifications::Create).to receive(:new).and_return(notification_service)
|
||||
allow(notification_service).to receive(:call)
|
||||
end
|
||||
|
||||
it 'translates :none to :incremental' do
|
||||
allow(generator_instance).to receive(:call).and_return(0)
|
||||
|
||||
described_class.new.perform(user.id, mode: :none)
|
||||
|
||||
expect(Tracks::Generator).to have_received(:new).with(
|
||||
user,
|
||||
start_at: nil,
|
||||
end_at: nil,
|
||||
mode: :incremental
|
||||
)
|
||||
expect(Notifications::Create).to have_received(:new).with(
|
||||
user: user,
|
||||
kind: :info,
|
||||
title: 'Tracks Generated',
|
||||
content: 'Created 0 tracks from your location data. Check your tracks section to view them.'
|
||||
)
|
||||
end
|
||||
|
||||
it 'translates :daily to :daily' do
|
||||
allow(generator_instance).to receive(:call).and_return(0)
|
||||
|
||||
described_class.new.perform(user.id, mode: :daily)
|
||||
|
||||
expect(Tracks::Generator).to have_received(:new).with(
|
||||
user,
|
||||
start_at: nil,
|
||||
end_at: nil,
|
||||
mode: :daily
|
||||
)
|
||||
expect(Notifications::Create).to have_received(:new).with(
|
||||
user: user,
|
||||
kind: :info,
|
||||
title: 'Tracks Generated',
|
||||
content: 'Created 0 tracks from your location data. Check your tracks section to view them.'
|
||||
)
|
||||
end
|
||||
|
||||
it 'translates other modes to :bulk' do
|
||||
allow(generator_instance).to receive(:call).and_return(0)
|
||||
|
||||
described_class.new.perform(user.id, mode: :replace)
|
||||
|
||||
expect(Tracks::Generator).to have_received(:new).with(
|
||||
user,
|
||||
start_at: nil,
|
||||
end_at: nil,
|
||||
mode: :bulk
|
||||
)
|
||||
expect(Notifications::Create).to have_received(:new).with(
|
||||
user: user,
|
||||
kind: :info,
|
||||
title: 'Tracks Generated',
|
||||
content: 'Created 0 tracks from your location data. Check your tracks section to view them.'
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when generator raises an error' do
|
||||
let(:error_message) { 'Something went wrong' }
|
||||
let(:notification_service) { instance_double(Notifications::Create) }
|
||||
|
|
@ -175,12 +105,13 @@ RSpec.describe Tracks::CreateJob, type: :job do
|
|||
end
|
||||
|
||||
context 'when user does not exist' do
|
||||
it 'handles the error gracefully and creates error notification' do
|
||||
before do
|
||||
allow(User).to receive(:find).with(999).and_raise(ActiveRecord::RecordNotFound)
|
||||
allow(ExceptionReporter).to receive(:call)
|
||||
allow(Notifications::Create).to receive(:new).and_return(instance_double(Notifications::Create, call: nil))
|
||||
end
|
||||
|
||||
# Should not raise an error because it's caught by the rescue block
|
||||
it 'handles the error gracefully and creates error notification' do
|
||||
expect { described_class.new.perform(999) }.not_to raise_error
|
||||
|
||||
expect(ExceptionReporter).to have_received(:call)
|
||||
|
|
@ -188,15 +119,14 @@ RSpec.describe Tracks::CreateJob, type: :job do
|
|||
end
|
||||
|
||||
context 'when tracks are deleted and recreated' do
|
||||
it 'returns the correct count of newly created tracks' do
|
||||
# Create some existing tracks first
|
||||
create_list(:track, 3, user: user)
|
||||
let(:existing_tracks) { create_list(:track, 3, user: user) }
|
||||
|
||||
# Mock the generator to simulate deleting existing tracks and creating new ones
|
||||
# This should return the count of newly created tracks, not the difference
|
||||
before do
|
||||
allow(generator_instance).to receive(:call).and_return(2)
|
||||
end
|
||||
|
||||
described_class.new.perform(user.id, mode: :bulk)
|
||||
it 'returns the correct count of newly created tracks' do
|
||||
described_class.new.perform(user.id, mode: :incremental)
|
||||
|
||||
expect(Tracks::Generator).to have_received(:new).with(
|
||||
user,
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ RSpec.describe Trip, type: :model do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#recalculate_distance!' do
|
||||
describe '#recalculate_distance!' do
|
||||
it 'recalculates and saves the distance' do
|
||||
original_distance = trip.distance
|
||||
|
||||
|
|
|
|||
|
|
@ -41,9 +41,6 @@ RSpec.configure do |config|
|
|||
|
||||
config.before(:suite) do
|
||||
Rails.application.reload_routes!
|
||||
|
||||
# DatabaseCleaner.strategy = :transaction
|
||||
# DatabaseCleaner.clean_with(:truncation)
|
||||
end
|
||||
|
||||
config.before do
|
||||
|
|
@ -92,12 +89,6 @@ RSpec.configure do |config|
|
|||
config.after(:suite) do
|
||||
Rake::Task['rswag:generate'].invoke
|
||||
end
|
||||
|
||||
# config.around(:each) do |example|
|
||||
# DatabaseCleaner.cleaning do
|
||||
# example.run
|
||||
# end
|
||||
# end
|
||||
end
|
||||
|
||||
Shoulda::Matchers.configure do |config|
|
||||
|
|
|
|||
|
|
@ -5,95 +5,166 @@ require 'rails_helper'
|
|||
RSpec.describe TrackSerializer do
|
||||
describe '#call' do
|
||||
let(:user) { create(:user) }
|
||||
let(:track) { create(:track, user: user) }
|
||||
let(:serializer) { described_class.new(track) }
|
||||
|
||||
context 'when serializing user tracks with track IDs' do
|
||||
subject(:serializer) { described_class.new(user, track_ids).call }
|
||||
subject(:serialized_track) { serializer.call }
|
||||
|
||||
let!(:track1) { create(:track, user: user, start_at: 2.hours.ago, end_at: 1.hour.ago) }
|
||||
let!(:track2) { create(:track, user: user, start_at: 4.hours.ago, end_at: 3.hours.ago) }
|
||||
let!(:track3) { create(:track, user: user, start_at: 6.hours.ago, end_at: 5.hours.ago) }
|
||||
let(:track_ids) { [track1.id, track2.id] }
|
||||
it 'returns a hash with all required attributes' do
|
||||
expect(serialized_track).to be_a(Hash)
|
||||
expect(serialized_track.keys).to contain_exactly(
|
||||
:id, :start_at, :end_at, :distance, :avg_speed, :duration,
|
||||
:elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path
|
||||
)
|
||||
end
|
||||
|
||||
it 'returns an array of serialized tracks' do
|
||||
expect(serializer).to be_an(Array)
|
||||
expect(serializer.length).to eq(2)
|
||||
end
|
||||
it 'serializes the track ID correctly' do
|
||||
expect(serialized_track[:id]).to eq(track.id)
|
||||
end
|
||||
|
||||
it 'serializes each track correctly' do
|
||||
serialized_ids = serializer.map { |track| track[:id] }
|
||||
expect(serialized_ids).to contain_exactly(track1.id, track2.id)
|
||||
expect(serialized_ids).not_to include(track3.id)
|
||||
end
|
||||
it 'formats start_at as ISO8601 timestamp' do
|
||||
expect(serialized_track[:start_at]).to eq(track.start_at.iso8601)
|
||||
expect(serialized_track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
||||
end
|
||||
|
||||
it 'formats timestamps as ISO8601 for all tracks' do
|
||||
serializer.each do |track|
|
||||
expect(track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
||||
expect(track[:end_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
||||
end
|
||||
end
|
||||
it 'formats end_at as ISO8601 timestamp' do
|
||||
expect(serialized_track[:end_at]).to eq(track.end_at.iso8601)
|
||||
expect(serialized_track[:end_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
||||
end
|
||||
|
||||
it 'includes all required fields for each track' do
|
||||
serializer.each do |track|
|
||||
expect(track.keys).to contain_exactly(
|
||||
:id, :start_at, :end_at, :distance, :avg_speed, :duration,
|
||||
:elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path
|
||||
)
|
||||
end
|
||||
end
|
||||
it 'converts distance to integer' do
|
||||
expect(serialized_track[:distance]).to eq(track.distance.to_i)
|
||||
expect(serialized_track[:distance]).to be_a(Integer)
|
||||
end
|
||||
|
||||
it 'handles numeric values correctly' do
|
||||
serializer.each do |track|
|
||||
expect(track[:distance]).to be_a(Numeric)
|
||||
expect(track[:avg_speed]).to be_a(Numeric)
|
||||
expect(track[:duration]).to be_a(Numeric)
|
||||
expect(track[:elevation_gain]).to be_a(Numeric)
|
||||
expect(track[:elevation_loss]).to be_a(Numeric)
|
||||
expect(track[:elevation_max]).to be_a(Numeric)
|
||||
expect(track[:elevation_min]).to be_a(Numeric)
|
||||
end
|
||||
end
|
||||
it 'converts avg_speed to float' do
|
||||
expect(serialized_track[:avg_speed]).to eq(track.avg_speed.to_f)
|
||||
expect(serialized_track[:avg_speed]).to be_a(Float)
|
||||
end
|
||||
|
||||
it 'orders tracks by start_at in ascending order' do
|
||||
serialized_tracks = serializer
|
||||
expect(serialized_tracks.first[:id]).to eq(track2.id) # Started 4 hours ago
|
||||
expect(serialized_tracks.second[:id]).to eq(track1.id) # Started 2 hours ago
|
||||
it 'serializes duration as numeric value' do
|
||||
expect(serialized_track[:duration]).to eq(track.duration)
|
||||
expect(serialized_track[:duration]).to be_a(Numeric)
|
||||
end
|
||||
|
||||
it 'serializes elevation_gain as numeric value' do
|
||||
expect(serialized_track[:elevation_gain]).to eq(track.elevation_gain)
|
||||
expect(serialized_track[:elevation_gain]).to be_a(Numeric)
|
||||
end
|
||||
|
||||
it 'serializes elevation_loss as numeric value' do
|
||||
expect(serialized_track[:elevation_loss]).to eq(track.elevation_loss)
|
||||
expect(serialized_track[:elevation_loss]).to be_a(Numeric)
|
||||
end
|
||||
|
||||
it 'serializes elevation_max as numeric value' do
|
||||
expect(serialized_track[:elevation_max]).to eq(track.elevation_max)
|
||||
expect(serialized_track[:elevation_max]).to be_a(Numeric)
|
||||
end
|
||||
|
||||
it 'serializes elevation_min as numeric value' do
|
||||
expect(serialized_track[:elevation_min]).to eq(track.elevation_min)
|
||||
expect(serialized_track[:elevation_min]).to be_a(Numeric)
|
||||
end
|
||||
|
||||
it 'converts original_path to string' do
|
||||
expect(serialized_track[:original_path]).to eq(track.original_path.to_s)
|
||||
expect(serialized_track[:original_path]).to be_a(String)
|
||||
end
|
||||
|
||||
context 'with decimal distance values' do
|
||||
let(:track) { create(:track, user: user, distance: 1234.56) }
|
||||
|
||||
it 'truncates distance to integer' do
|
||||
expect(serialized_track[:distance]).to eq(1234)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when track IDs belong to different users' do
|
||||
subject(:serializer) { described_class.new(user, track_ids).call }
|
||||
context 'with decimal avg_speed values' do
|
||||
let(:track) { create(:track, user: user, avg_speed: 25.75) }
|
||||
|
||||
let(:other_user) { create(:user) }
|
||||
let!(:user_track) { create(:track, user: user) }
|
||||
let!(:other_user_track) { create(:track, user: other_user) }
|
||||
let(:track_ids) { [user_track.id, other_user_track.id] }
|
||||
|
||||
it 'only returns tracks belonging to the specified user' do
|
||||
serialized_ids = serializer.map { |track| track[:id] }
|
||||
expect(serialized_ids).to contain_exactly(user_track.id)
|
||||
expect(serialized_ids).not_to include(other_user_track.id)
|
||||
it 'converts avg_speed to float' do
|
||||
expect(serialized_track[:avg_speed]).to eq(25.75)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when track IDs array is empty' do
|
||||
subject(:serializer) { described_class.new(user, []).call }
|
||||
context 'with different original_path formats' do
|
||||
let(:track) { create(:track, user: user, original_path: 'LINESTRING(0 0, 1 1, 2 2)') }
|
||||
|
||||
it 'returns an empty array' do
|
||||
expect(serializer).to eq([])
|
||||
it 'converts geometry to WKT string format' do
|
||||
expect(serialized_track[:original_path]).to eq('LINESTRING (0 0, 1 1, 2 2)')
|
||||
expect(serialized_track[:original_path]).to be_a(String)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when track IDs contain non-existent IDs' do
|
||||
subject(:serializer) { described_class.new(user, track_ids).call }
|
||||
context 'with zero values' do
|
||||
let(:track) do
|
||||
create(:track, user: user,
|
||||
distance: 0,
|
||||
avg_speed: 0.0,
|
||||
duration: 0,
|
||||
elevation_gain: 0,
|
||||
elevation_loss: 0,
|
||||
elevation_max: 0,
|
||||
elevation_min: 0)
|
||||
end
|
||||
|
||||
let!(:existing_track) { create(:track, user: user) }
|
||||
let(:track_ids) { [existing_track.id, 999999] }
|
||||
it 'handles zero values correctly' do
|
||||
expect(serialized_track[:distance]).to eq(0)
|
||||
expect(serialized_track[:avg_speed]).to eq(0.0)
|
||||
expect(serialized_track[:duration]).to eq(0)
|
||||
expect(serialized_track[:elevation_gain]).to eq(0)
|
||||
expect(serialized_track[:elevation_loss]).to eq(0)
|
||||
expect(serialized_track[:elevation_max]).to eq(0)
|
||||
expect(serialized_track[:elevation_min]).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
it 'only returns existing tracks' do
|
||||
serialized_ids = serializer.map { |track| track[:id] }
|
||||
expect(serialized_ids).to contain_exactly(existing_track.id)
|
||||
expect(serializer.length).to eq(1)
|
||||
context 'with very large values' do
|
||||
let(:track) do
|
||||
create(:track, user: user,
|
||||
distance: 1_000_000.0,
|
||||
avg_speed: 999.99,
|
||||
duration: 86_400, # 24 hours in seconds
|
||||
elevation_gain: 10_000,
|
||||
elevation_loss: 8_000,
|
||||
elevation_max: 5_000,
|
||||
elevation_min: 0)
|
||||
end
|
||||
|
||||
it 'handles large values correctly' do
|
||||
expect(serialized_track[:distance]).to eq(1_000_000)
|
||||
expect(serialized_track[:avg_speed]).to eq(999.99)
|
||||
expect(serialized_track[:duration]).to eq(86_400)
|
||||
expect(serialized_track[:elevation_gain]).to eq(10_000)
|
||||
expect(serialized_track[:elevation_loss]).to eq(8_000)
|
||||
expect(serialized_track[:elevation_max]).to eq(5_000)
|
||||
expect(serialized_track[:elevation_min]).to eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with different timestamp formats' do
|
||||
let(:start_time) { Time.current }
|
||||
let(:end_time) { start_time + 1.hour }
|
||||
let(:track) { create(:track, user: user, start_at: start_time, end_at: end_time) }
|
||||
|
||||
it 'formats timestamps consistently' do
|
||||
expect(serialized_track[:start_at]).to eq(start_time.iso8601)
|
||||
expect(serialized_track[:end_at]).to eq(end_time.iso8601)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#initialize' do
|
||||
let(:track) { create(:track) }
|
||||
|
||||
it 'accepts a track parameter' do
|
||||
expect { described_class.new(track) }.not_to raise_error
|
||||
end
|
||||
|
||||
it 'stores the track instance' do
|
||||
serializer = described_class.new(track)
|
||||
expect(serializer.instance_variable_get(:@track)).to eq(track)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
99
spec/serializers/tracks_serializer_spec.rb
Normal file
99
spec/serializers/tracks_serializer_spec.rb
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe TracksSerializer do
|
||||
describe '#call' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'when serializing user tracks with track IDs' do
|
||||
subject(:serializer) { described_class.new(user, track_ids).call }
|
||||
|
||||
let!(:track1) { create(:track, user: user, start_at: 2.hours.ago, end_at: 1.hour.ago) }
|
||||
let!(:track2) { create(:track, user: user, start_at: 4.hours.ago, end_at: 3.hours.ago) }
|
||||
let!(:track3) { create(:track, user: user, start_at: 6.hours.ago, end_at: 5.hours.ago) }
|
||||
let(:track_ids) { [track1.id, track2.id] }
|
||||
|
||||
it 'returns an array of serialized tracks' do
|
||||
expect(serializer).to be_an(Array)
|
||||
expect(serializer.length).to eq(2)
|
||||
end
|
||||
|
||||
it 'serializes each track correctly' do
|
||||
serialized_ids = serializer.map { |track| track[:id] }
|
||||
expect(serialized_ids).to contain_exactly(track1.id, track2.id)
|
||||
expect(serialized_ids).not_to include(track3.id)
|
||||
end
|
||||
|
||||
it 'formats timestamps as ISO8601 for all tracks' do
|
||||
serializer.each do |track|
|
||||
expect(track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
||||
expect(track[:end_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
||||
end
|
||||
end
|
||||
|
||||
it 'includes all required fields for each track' do
|
||||
serializer.each do |track|
|
||||
expect(track.keys).to contain_exactly(
|
||||
:id, :start_at, :end_at, :distance, :avg_speed, :duration,
|
||||
:elevation_gain, :elevation_loss, :elevation_max, :elevation_min, :original_path
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
it 'handles numeric values correctly' do
|
||||
serializer.each do |track|
|
||||
expect(track[:distance]).to be_a(Numeric)
|
||||
expect(track[:avg_speed]).to be_a(Numeric)
|
||||
expect(track[:duration]).to be_a(Numeric)
|
||||
expect(track[:elevation_gain]).to be_a(Numeric)
|
||||
expect(track[:elevation_loss]).to be_a(Numeric)
|
||||
expect(track[:elevation_max]).to be_a(Numeric)
|
||||
expect(track[:elevation_min]).to be_a(Numeric)
|
||||
end
|
||||
end
|
||||
|
||||
it 'orders tracks by start_at in ascending order' do
|
||||
serialized_tracks = serializer
|
||||
expect(serialized_tracks.first[:id]).to eq(track2.id) # Started 4 hours ago
|
||||
expect(serialized_tracks.second[:id]).to eq(track1.id) # Started 2 hours ago
|
||||
end
|
||||
end
|
||||
|
||||
context 'when track IDs belong to different users' do
|
||||
subject(:serializer) { described_class.new(user, track_ids).call }
|
||||
|
||||
let(:other_user) { create(:user) }
|
||||
let!(:user_track) { create(:track, user: user) }
|
||||
let!(:other_user_track) { create(:track, user: other_user) }
|
||||
let(:track_ids) { [user_track.id, other_user_track.id] }
|
||||
|
||||
it 'only returns tracks belonging to the specified user' do
|
||||
serialized_ids = serializer.map { |track| track[:id] }
|
||||
expect(serialized_ids).to contain_exactly(user_track.id)
|
||||
expect(serialized_ids).not_to include(other_user_track.id)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when track IDs array is empty' do
|
||||
subject(:serializer) { described_class.new(user, []).call }
|
||||
|
||||
it 'returns an empty array' do
|
||||
expect(serializer).to eq([])
|
||||
end
|
||||
end
|
||||
|
||||
context 'when track IDs contain non-existent IDs' do
|
||||
subject(:serializer) { described_class.new(user, track_ids).call }
|
||||
|
||||
let!(:existing_track) { create(:track, user: user) }
|
||||
let(:track_ids) { [existing_track.id, 999999] }
|
||||
|
||||
it 'only returns existing tracks' do
|
||||
serialized_ids = serializer.map { |track| track[:id] }
|
||||
expect(serialized_ids).to contain_exactly(existing_track.id)
|
||||
expect(serializer.length).to eq(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -30,7 +30,7 @@ RSpec.describe Tracks::IncrementalProcessor do
|
|||
|
||||
it 'processes first point' do
|
||||
expect(Tracks::CreateJob).to receive(:perform_later)
|
||||
.with(user.id, start_at: nil, end_at: nil, mode: :none)
|
||||
.with(user.id, start_at: nil, end_at: nil, mode: :incremental)
|
||||
processor.call
|
||||
end
|
||||
end
|
||||
|
|
@ -47,7 +47,7 @@ RSpec.describe Tracks::IncrementalProcessor do
|
|||
|
||||
it 'processes when time threshold exceeded' do
|
||||
expect(Tracks::CreateJob).to receive(:perform_later)
|
||||
.with(user.id, start_at: nil, end_at: Time.zone.at(previous_point.timestamp), mode: :none)
|
||||
.with(user.id, start_at: nil, end_at: Time.zone.at(previous_point.timestamp), mode: :incremental)
|
||||
processor.call
|
||||
end
|
||||
end
|
||||
|
|
@ -65,7 +65,7 @@ RSpec.describe Tracks::IncrementalProcessor do
|
|||
|
||||
it 'uses existing track end time as start_at' do
|
||||
expect(Tracks::CreateJob).to receive(:perform_later)
|
||||
.with(user.id, start_at: existing_track.end_at, end_at: Time.zone.at(previous_point.timestamp), mode: :none)
|
||||
.with(user.id, start_at: existing_track.end_at, end_at: Time.zone.at(previous_point.timestamp), mode: :incremental)
|
||||
processor.call
|
||||
end
|
||||
end
|
||||
|
|
@ -88,7 +88,7 @@ RSpec.describe Tracks::IncrementalProcessor do
|
|||
|
||||
it 'processes when distance threshold exceeded' do
|
||||
expect(Tracks::CreateJob).to receive(:perform_later)
|
||||
.with(user.id, start_at: nil, end_at: Time.zone.at(previous_point.timestamp), mode: :none)
|
||||
.with(user.id, start_at: nil, end_at: Time.zone.at(previous_point.timestamp), mode: :incremental)
|
||||
processor.call
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -75,7 +75,6 @@ RSpec.describe Visits::Suggest do
|
|||
end
|
||||
|
||||
context 'when reverse geocoding is enabled' do
|
||||
# Use a different time range to avoid interference with main tests
|
||||
let(:reverse_geocoding_start_at) { Time.zone.local(2020, 6, 1, 0, 0, 0) }
|
||||
let(:reverse_geocoding_end_at) { Time.zone.local(2020, 6, 1, 2, 0, 0) }
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
RSpec.configure do |config|
|
||||
config.before(:each) do
|
||||
# Clear the cache before each test
|
||||
Rails.cache.clear
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue