mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 17:51:39 -05:00
Extract logic to services from TracksController#index and add tests
This commit is contained in:
parent
7f38e07d4d
commit
af1bb773ed
5 changed files with 226 additions and 40 deletions
|
|
@ -2,50 +2,15 @@
|
|||
|
||||
class Api::V1::TracksController < ApiController
|
||||
def index
|
||||
tracks = current_api_user.tracks
|
||||
tracks_query = Tracks::IndexQuery.new(user: current_api_user, params: params)
|
||||
paginated_tracks = tracks_query.call
|
||||
|
||||
# Date range filtering (overlap logic)
|
||||
if params[:start_at].present? && params[:end_at].present?
|
||||
start_at = Time.zone.parse(params[:start_at])
|
||||
end_at = Time.zone.parse(params[:end_at])
|
||||
geojson = Tracks::GeojsonSerializer.new(paginated_tracks).call
|
||||
|
||||
# Show tracks that overlap: end_at >= start_filter AND start_at <= end_filter
|
||||
tracks = tracks.where('end_at >= ? AND start_at <= ?', start_at, end_at)
|
||||
tracks_query.pagination_headers(paginated_tracks).each do |header, value|
|
||||
response.set_header(header, value)
|
||||
end
|
||||
|
||||
# Pagination (Kaminari)
|
||||
tracks = tracks
|
||||
.order(start_at: :desc)
|
||||
.page(params[:page])
|
||||
.per(params[:per_page] || 100)
|
||||
|
||||
# Serialize to GeoJSON format
|
||||
features = tracks.map do |track|
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: RGeo::GeoJSON.encode(track.original_path),
|
||||
properties: {
|
||||
id: track.id,
|
||||
color: '#ff0000', # Red color
|
||||
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
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
geojson = {
|
||||
type: 'FeatureCollection',
|
||||
features: features
|
||||
}
|
||||
|
||||
# Add pagination headers
|
||||
response.set_header('X-Current-Page', tracks.current_page.to_s)
|
||||
response.set_header('X-Total-Pages', tracks.total_pages.to_s)
|
||||
response.set_header('X-Total-Count', tracks.total_count.to_s)
|
||||
|
||||
render json: geojson
|
||||
end
|
||||
end
|
||||
|
|
|
|||
42
app/serializers/tracks/geojson_serializer.rb
Normal file
42
app/serializers/tracks/geojson_serializer.rb
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Tracks
|
||||
class GeojsonSerializer
|
||||
DEFAULT_COLOR = '#ff0000'
|
||||
|
||||
def initialize(tracks)
|
||||
@tracks = Array.wrap(tracks)
|
||||
end
|
||||
|
||||
def call
|
||||
{
|
||||
type: 'FeatureCollection',
|
||||
features: tracks.map { |track| feature_for(track) }
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :tracks
|
||||
|
||||
def feature_for(track)
|
||||
{
|
||||
type: 'Feature',
|
||||
geometry: RGeo::GeoJSON.encode(track.original_path),
|
||||
properties: properties_for(track)
|
||||
}
|
||||
end
|
||||
|
||||
def properties_for(track)
|
||||
{
|
||||
id: track.id,
|
||||
color: DEFAULT_COLOR,
|
||||
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
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
61
app/services/tracks/index_query.rb
Normal file
61
app/services/tracks/index_query.rb
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Tracks
|
||||
# Encapsulates filtering and pagination logic for API track listings
|
||||
class IndexQuery
|
||||
DEFAULT_PER_PAGE = 100
|
||||
|
||||
def initialize(user:, params: {})
|
||||
@user = user
|
||||
@params = params.with_indifferent_access
|
||||
end
|
||||
|
||||
def call
|
||||
scoped = user.tracks
|
||||
scoped = apply_date_range(scoped)
|
||||
|
||||
scoped
|
||||
.order(start_at: :desc)
|
||||
.page(page_param)
|
||||
.per(per_page_param)
|
||||
end
|
||||
|
||||
def pagination_headers(paginated_relation)
|
||||
{
|
||||
'X-Current-Page' => paginated_relation.current_page.to_s,
|
||||
'X-Total-Pages' => paginated_relation.total_pages.to_s,
|
||||
'X-Total-Count' => paginated_relation.total_count.to_s
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user, :params
|
||||
|
||||
def page_param
|
||||
candidate = params[:page].to_i
|
||||
candidate.positive? ? candidate : 1
|
||||
end
|
||||
|
||||
def per_page_param
|
||||
candidate = params[:per_page].to_i
|
||||
candidate.positive? ? candidate : DEFAULT_PER_PAGE
|
||||
end
|
||||
|
||||
def apply_date_range(scope)
|
||||
return scope unless params[:start_at].present? && params[:end_at].present?
|
||||
|
||||
start_at = parse_timestamp(params[:start_at])
|
||||
end_at = parse_timestamp(params[:end_at])
|
||||
return scope if start_at.blank? || end_at.blank?
|
||||
|
||||
scope.where('end_at >= ? AND start_at <= ?', start_at, end_at)
|
||||
end
|
||||
|
||||
def parse_timestamp(value)
|
||||
Time.zone.parse(value)
|
||||
rescue ArgumentError, TypeError
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
38
spec/serializers/tracks/geojson_serializer_spec.rb
Normal file
38
spec/serializers/tracks/geojson_serializer_spec.rb
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Tracks::GeojsonSerializer do
|
||||
let(:track) do
|
||||
create(:track,
|
||||
start_at: Time.zone.parse('2024-01-01 10:00'),
|
||||
end_at: Time.zone.parse('2024-01-01 11:00'),
|
||||
distance: 1234.56,
|
||||
avg_speed: 42.5,
|
||||
duration: 3600)
|
||||
end
|
||||
|
||||
describe '#call' do
|
||||
it 'returns a FeatureCollection structure' do
|
||||
result = described_class.new([track]).call
|
||||
|
||||
expect(result[:type]).to eq('FeatureCollection')
|
||||
expect(result[:features].length).to eq(1)
|
||||
end
|
||||
|
||||
it 'includes geometry and track properties' do
|
||||
feature = described_class.new([track]).call[:features].first
|
||||
|
||||
expect(feature[:geometry][:type]).to eq('LineString')
|
||||
expect(feature[:properties]).to include(
|
||||
id: track.id,
|
||||
color: '#ff0000',
|
||||
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
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
80
spec/services/tracks/index_query_spec.rb
Normal file
80
spec/services/tracks/index_query_spec.rb
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Tracks::IndexQuery do
|
||||
let(:user) { create(:user) }
|
||||
let(:params) { {} }
|
||||
let(:query) { described_class.new(user: user, params: params) }
|
||||
|
||||
describe '#call' do
|
||||
let!(:newest_track) do
|
||||
create(:track, user: user,
|
||||
start_at: Time.zone.parse('2024-01-03 10:00'),
|
||||
end_at: Time.zone.parse('2024-01-03 12:00'))
|
||||
end
|
||||
|
||||
let!(:older_track) do
|
||||
create(:track, user: user,
|
||||
start_at: Time.zone.parse('2024-01-01 10:00'),
|
||||
end_at: Time.zone.parse('2024-01-01 12:00'))
|
||||
end
|
||||
let!(:other_user_track) { create(:track) }
|
||||
|
||||
it 'returns tracks for the user ordered by start_at desc' do
|
||||
result = query.call
|
||||
|
||||
expect(result).to match_array([newest_track, older_track])
|
||||
expect(result.first).to eq(newest_track)
|
||||
expect(result).not_to include(other_user_track)
|
||||
end
|
||||
|
||||
context 'with pagination params' do
|
||||
let(:params) { { page: 1, per_page: 1 } }
|
||||
|
||||
it 'applies pagination settings' do
|
||||
result = query.call
|
||||
expect(result.count).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with overlapping date range filter' do
|
||||
let(:params) do
|
||||
{
|
||||
start_at: '2024-01-02T00:00:00Z',
|
||||
end_at: '2024-01-04T00:00:00Z'
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns tracks that overlap the date range' do
|
||||
result = query.call
|
||||
|
||||
expect(result).to include(newest_track)
|
||||
expect(result).not_to include(older_track)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid date params' do
|
||||
let(:params) { { start_at: 'invalid', end_at: 'also-invalid' } }
|
||||
|
||||
it 'ignores the invalid filter and returns all tracks' do
|
||||
result = query.call
|
||||
expect(result.count).to eq(2)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#pagination_headers' do
|
||||
it 'builds the pagination header hash' do
|
||||
paginated_relation = double('paginated', current_page: 2, total_pages: 5, total_count: 12)
|
||||
|
||||
headers = query.pagination_headers(paginated_relation)
|
||||
|
||||
expect(headers).to eq(
|
||||
'X-Current-Page' => '2',
|
||||
'X-Total-Pages' => '5',
|
||||
'X-Total-Count' => '12'
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue