From 7b876ea754e72849384af4c4662a31da2902fd51 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Tue, 24 Sep 2024 00:10:39 +0200 Subject: [PATCH] Fix GPX export timestamps and add slim version of points --- .app_version | 2 +- CHANGELOG.md | 16 ++++ app/controllers/api/v1/points_controller.rb | 10 +- app/serializers/point_serializer.rb | 2 +- app/serializers/points/gpx_serializer.rb | 13 ++- app/serializers/slim_point_serializer.rb | 19 ++++ spec/factories/points.rb | 44 ++++----- spec/requests/api/v1/points_spec.rb | 91 +++++++++++++++---- spec/serializers/points/gpx_serializer.rb | 17 ---- .../serializers/points/gpx_serializer_spec.rb | 30 ++++++ 10 files changed, 180 insertions(+), 64 deletions(-) create mode 100644 app/serializers/slim_point_serializer.rb delete mode 100644 spec/serializers/points/gpx_serializer.rb create mode 100644 spec/serializers/points/gpx_serializer_spec.rb diff --git a/.app_version b/.app_version index ac4a7962..3393b5fd 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.14.3 +0.14.4 diff --git a/CHANGELOG.md b/CHANGELOG.md index d4b285f5..a0a0b103 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,12 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +# [0.14.4] - 2024-09-24 + +### Fixed + +- GPX export now has time and elevation elements for each point + +### Changed + +- `GET /api/v1/points` will no longer return `raw_data` attribute for each point as it's a bit too much + +### Added + +- "Slim" version of `GET /api/v1/points`: pass optional param `?slim=true` to it and it will return only latitude, longitude and timestamp + + # [0.14.3] — 2024-09-21 ### Fixed - Optimize order of the dockerfiles to leverage layer caching by @JoeyEamigh - Add support for alternate postgres ports and db names in docker by @JoeyEamigh +- Creating exports directory if it doesn't exist by @tetebueno ## [0.14.1] — 2024-09-16 diff --git a/app/controllers/api/v1/points_controller.rb b/app/controllers/api/v1/points_controller.rb index fbf200c4..6079b60c 100644 --- a/app/controllers/api/v1/points_controller.rb +++ b/app/controllers/api/v1/points_controller.rb @@ -11,11 +11,13 @@ class Api::V1::PointsController < ApiController .order(:timestamp) .page(params[:page]) .per(params[:per_page] || 100) + + serialized_points = points.map { |point| point_serializer.new(point).call } response.set_header('X-Current-Page', points.current_page.to_s) response.set_header('X-Total-Pages', points.total_pages.to_s) - render json: points + render json: serialized_points end def destroy @@ -24,4 +26,10 @@ class Api::V1::PointsController < ApiController render json: { message: 'Point deleted successfully' } end + + private + + def point_serializer + params[:slim] ? SlimPointSerializer : PointSerializer + end end diff --git a/app/serializers/point_serializer.rb b/app/serializers/point_serializer.rb index 4bbd7ad0..270e3e25 100644 --- a/app/serializers/point_serializer.rb +++ b/app/serializers/point_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class PointSerializer - EXCLUDED_ATTRIBUTES = %w[created_at updated_at visit_id id import_id user_id].freeze + EXCLUDED_ATTRIBUTES = %w[created_at updated_at visit_id id import_id user_id raw_data].freeze def initialize(point) @point = point diff --git a/app/serializers/points/gpx_serializer.rb b/app/serializers/points/gpx_serializer.rb index c52c1e9b..74b47473 100644 --- a/app/serializers/points/gpx_serializer.rb +++ b/app/serializers/points/gpx_serializer.rb @@ -6,9 +6,18 @@ class Points::GpxSerializer end def call - geojson_data = Points::GeojsonSerializer.new(points).call + gpx = GPX::GPXFile.new - GPX::GeoJSON.convert_to_gpx(geojson_data:) + points.each do |point| + gpx.waypoints << GPX::Waypoint.new( + lat: point.latitude.to_f, + lon: point.longitude.to_f, + time: point.recorded_at.strftime('%FT%R:%SZ'), + ele: point.altitude.to_f + ) + end + + gpx end private diff --git a/app/serializers/slim_point_serializer.rb b/app/serializers/slim_point_serializer.rb new file mode 100644 index 00000000..b9b28d3c --- /dev/null +++ b/app/serializers/slim_point_serializer.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class SlimPointSerializer + def initialize(point) + @point = point + end + + def call + { + latitude: point.latitude, + longitude: point.longitude, + timestamp: point.timestamp + } + end + + private + + attr_reader :point +end diff --git a/spec/factories/points.rb b/spec/factories/points.rb index 7e327c61..ab4678a4 100644 --- a/spec/factories/points.rb +++ b/spec/factories/points.rb @@ -2,29 +2,29 @@ FactoryBot.define do factory :point do - battery_status { 1 } - ping { 'MyString' } - battery { 1 } - topic { 'MyString' } - altitude { 1 } - longitude { 'MyString' } - velocity { 'MyString' } - trigger { 1 } - bssid { 'MyString' } - ssid { 'MyString' } - connection { 1 } + battery_status { 1 } + ping { 'MyString' } + battery { 1 } + topic { 'MyString' } + altitude { 1 } + longitude { FFaker::Geolocation.lng } + velocity { 0 } + trigger { 1 } + bssid { 'MyString' } + ssid { 'MyString' } + connection { 1 } vertical_accuracy { 1 } - accuracy { 1 } - timestamp { 1 } - latitude { 'MyString' } - mode { 1 } - inrids { 'MyString' } - in_regions { 'MyString' } - raw_data { '' } - tracker_id { 'MyString' } - import_id { '' } - city { nil } - country { nil } + accuracy { 1 } + timestamp { 1.year.ago.to_i } + latitude { FFaker::Geolocation.lat } + mode { 1 } + inrids { 'MyString' } + in_regions { 'MyString' } + raw_data { '' } + tracker_id { 'MyString' } + import_id { '' } + city { nil } + country { nil } user trait :with_geodata do diff --git a/spec/requests/api/v1/points_spec.rb b/spec/requests/api/v1/points_spec.rb index d5d8957a..32839871 100644 --- a/spec/requests/api/v1/points_spec.rb +++ b/spec/requests/api/v1/points_spec.rb @@ -7,39 +7,90 @@ RSpec.describe 'Api::V1::Points', type: :request do let!(:points) { create_list(:point, 150, user:) } describe 'GET /index' do - it 'renders a successful response' do - get api_v1_points_url(api_key: user.api_key) + context 'when regular version of points is requested' do + it 'renders a successful response' do + get api_v1_points_url(api_key: user.api_key) - expect(response).to be_successful + expect(response).to be_successful + end + + it 'returns a list of points' do + get api_v1_points_url(api_key: user.api_key) + + expect(response).to have_http_status(:ok) + + json_response = JSON.parse(response.body) + + expect(json_response.size).to eq(100) + end + + it 'returns a list of points with pagination' do + get api_v1_points_url(api_key: user.api_key, page: 2, per_page: 10) + + expect(response).to have_http_status(:ok) + + json_response = JSON.parse(response.body) + + expect(json_response.size).to eq(10) + end + + it 'returns a list of points with pagination headers' do + get api_v1_points_url(api_key: user.api_key, page: 2, per_page: 10) + + expect(response).to have_http_status(:ok) + + expect(response.headers['X-Current-Page']).to eq('2') + expect(response.headers['X-Total-Pages']).to eq('15') + end end - it 'returns a list of points' do - get api_v1_points_url(api_key: user.api_key) + context 'when slim version of points is requested' do + it 'renders a successful response' do + get api_v1_points_url(api_key: user.api_key, slim: true) - expect(response).to have_http_status(:ok) + expect(response).to be_successful + end - json_response = JSON.parse(response.body) + it 'returns a list of points' do + get api_v1_points_url(api_key: user.api_key, slim: true) - expect(json_response.size).to eq(100) - end + expect(response).to have_http_status(:ok) - it 'returns a list of points with pagination' do - get api_v1_points_url(api_key: user.api_key, page: 2, per_page: 10) + json_response = JSON.parse(response.body) - expect(response).to have_http_status(:ok) + expect(json_response.size).to eq(100) + end - json_response = JSON.parse(response.body) + it 'returns a list of points with pagination' do + get api_v1_points_url(api_key: user.api_key, slim: true, page: 2, per_page: 10) - expect(json_response.size).to eq(10) - end + expect(response).to have_http_status(:ok) - it 'returns a list of points with pagination headers' do - get api_v1_points_url(api_key: user.api_key, page: 2, per_page: 10) + json_response = JSON.parse(response.body) - expect(response).to have_http_status(:ok) + expect(json_response.size).to eq(10) + end - expect(response.headers['X-Current-Page']).to eq('2') - expect(response.headers['X-Total-Pages']).to eq('15') + it 'returns a list of points with pagination headers' do + get api_v1_points_url(api_key: user.api_key, slim: true, page: 2, per_page: 10) + + expect(response).to have_http_status(:ok) + + expect(response.headers['X-Current-Page']).to eq('2') + expect(response.headers['X-Total-Pages']).to eq('15') + end + + it 'returns a list of points with slim attributes' do + get api_v1_points_url(api_key: user.api_key, slim: true) + + expect(response).to have_http_status(:ok) + + json_response = JSON.parse(response.body) + + json_response.each do |point| + expect(point.keys).to eq(%w[latitude longitude timestamp]) + end + end end end end diff --git a/spec/serializers/points/gpx_serializer.rb b/spec/serializers/points/gpx_serializer.rb deleted file mode 100644 index 9fee087e..00000000 --- a/spec/serializers/points/gpx_serializer.rb +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -require 'rails_helper' - -RSpec.describe Points::GpxSerializer do - describe '#call' do - subject(:serializer) { described_class.new(points).call } - - let(:points) { create_list(:point, 3) } - let(:geojson_data) { Points::GeojsonSerializer.new(points).call } - let(:gpx) { GPX::GeoJSON.convert_to_gpx(geojson_data:) } - - it 'returns JSON' do - expect(serializer).to be_a(GPX::GPXFile) - end - end -end diff --git a/spec/serializers/points/gpx_serializer_spec.rb b/spec/serializers/points/gpx_serializer_spec.rb new file mode 100644 index 00000000..21b5489d --- /dev/null +++ b/spec/serializers/points/gpx_serializer_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Points::GpxSerializer do + describe '#call' do + subject(:serializer) { described_class.new(points).call } + + let(:points) { create_list(:point, 3) } + let(:geojson_data) { Points::GeojsonSerializer.new(points).call } + let(:gpx) { GPX::GeoJSON.convert_to_gpx(geojson_data:) } + + it 'returns GPX file' do + expect(serializer).to be_a(GPX::GPXFile) + end + + it 'includes waypoints' do + expect(serializer.waypoints.size).to eq(3) + end + + it 'includes waypoints with correct attributes' do + serializer.waypoints.each_with_index do |waypoint, index| + point = points[index] + expect(waypoint.lat).to eq(point.latitude) + expect(waypoint.lon).to eq(point.longitude) + expect(waypoint.time).to eq(point.recorded_at.strftime('%FT%R:%SZ')) + end + end + end +end