Add optional order query parameter to GET /api/v1/points

This commit is contained in:
Eugene Burmakin 2024-10-02 21:29:56 +02:00
parent 6aaab424fe
commit df430851ce
14 changed files with 60 additions and 29 deletions

View file

@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
### Fixed ### Fixed
- Now you can use http protocol for the Photon API host if you don't have SSL certificate for it - Now you can use http protocol for the Photon API host if you don't have SSL certificate for it
- For stats, total distance per month might have been not equal to the sum of distances per day. Now it's fixed and values are equal
### Added
- `GET /api/v1/points` can now accept optional `?order=asc` query parameter to return points in ascending order by timestamp. `?order=desc` is still available to return points in descending order by timestamp.
# [0.14.6] - 2024-29-30 # [0.14.6] - 2024-29-30

View file

@ -3,12 +3,13 @@
class Api::V1::PointsController < ApiController class Api::V1::PointsController < ApiController
def index def index
start_at = params[:start_at]&.to_datetime&.to_i start_at = params[:start_at]&.to_datetime&.to_i
end_at = params[:end_at]&.to_datetime&.to_i || Time.zone.now.to_i end_at = params[:end_at]&.to_datetime&.to_i || Time.zone.now.to_i
order = params[:order] || 'desc'
points = current_api_user points = current_api_user
.tracked_points .tracked_points
.where(timestamp: start_at..end_at) .where(timestamp: start_at..end_at)
.order(:timestamp) .order(timestamp: order)
.page(params[:page]) .page(params[:page])
.per(params[:per_page] || 100) .per(params[:per_page] || 100)

View file

@ -5,9 +5,9 @@ class MapController < ApplicationController
def index def index
@points = points @points = points
.without_raw_data .without_raw_data
.where('timestamp >= ? AND timestamp <= ?', start_at, end_at) .where('timestamp >= ? AND timestamp <= ?', start_at, end_at)
.order(timestamp: :asc) .order(timestamp: :asc)
@countries_and_cities = CountriesAndCities.new(@points).call @countries_and_cities = CountriesAndCities.new(@points).call
@coordinates = @coordinates =
@ -38,7 +38,7 @@ class MapController < ApplicationController
@coordinates.each_cons(2) do @coordinates.each_cons(2) do
@distance += Geocoder::Calculations.distance_between( @distance += Geocoder::Calculations.distance_between(
[_1[0], _1[1]], [_2[0], _2[1]], units: DISTANCE_UNIT.to_sym [_1[0], _1[1]], [_2[0], _2[1]], units: DISTANCE_UNIT
) )
end end

View file

@ -17,7 +17,7 @@ class Stat < ApplicationRecord
points.each_cons(2) do |point1, point2| points.each_cons(2) do |point1, point2|
distance = Geocoder::Calculations.distance_between( distance = Geocoder::Calculations.distance_between(
[point1.latitude, point1.longitude], [point2.latitude, point2.longitude] point1.to_coordinates, point2.to_coordinates, units: ::DISTANCE_UNIT
) )
data[:distance] += distance data[:distance] += distance

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
class PointSerializer class PointSerializer
EXCLUDED_ATTRIBUTES = %w[created_at updated_at visit_id id import_id user_id raw_data].freeze EXCLUDED_ATTRIBUTES = %w[created_at updated_at visit_id import_id user_id raw_data].freeze
def initialize(point) def initialize(point)
@point = point @point = point

View file

@ -31,14 +31,14 @@ class Areas::Visits::Create
def area_points(area) def area_points(area)
area_radius = area_radius =
if ::DISTANCE_UNIT.to_sym == :km if ::DISTANCE_UNIT == :km
area.radius / 1000.0 area.radius / 1000.0
else else
area.radius / 1609.344 area.radius / 1609.344
end end
points = Point.where(user_id: user.id) points = Point.where(user_id: user.id)
.near([area.latitude, area.longitude], area_radius, units: DISTANCE_UNIT.to_sym) .near([area.latitude, area.longitude], area_radius, units: DISTANCE_UNIT)
.order(timestamp: :asc) .order(timestamp: :asc)
# check if all points within the area are assigned to a visit # check if all points within the area are assigned to a visit

View file

@ -41,22 +41,15 @@ class CreateStats
return if points.empty? return if points.empty?
stat = Stat.find_or_initialize_by(year:, month:, user:) stat = Stat.find_or_initialize_by(year:, month:, user:)
stat.distance = distance(points) distance_by_day = stat.distance_by_day
stat.daily_distance = distance_by_day
stat.distance = distance(distance_by_day)
stat.toponyms = toponyms(points) stat.toponyms = toponyms(points)
stat.daily_distance = stat.distance_by_day
stat.save stat.save
end end
def distance(points) def distance(distance_by_day)
distance = 0 distance_by_day.sum { |d| d[1] }
points.each_cons(2) do
distance += Geocoder::Calculations.distance_between(
[_1.latitude, _1.longitude], [_2.latitude, _2.longitude], units: DISTANCE_UNIT.to_sym
)
end
distance
end end
def toponyms(points) def toponyms(points)

View file

@ -3,4 +3,4 @@
MIN_MINUTES_SPENT_IN_CITY = ENV.fetch('MIN_MINUTES_SPENT_IN_CITY', 60).to_i MIN_MINUTES_SPENT_IN_CITY = ENV.fetch('MIN_MINUTES_SPENT_IN_CITY', 60).to_i
REVERSE_GEOCODING_ENABLED = ENV.fetch('REVERSE_GEOCODING_ENABLED', 'true') == 'true' REVERSE_GEOCODING_ENABLED = ENV.fetch('REVERSE_GEOCODING_ENABLED', 'true') == 'true'
PHOTON_API_HOST = ENV.fetch('PHOTON_API_HOST', nil) PHOTON_API_HOST = ENV.fetch('PHOTON_API_HOST', nil)
DISTANCE_UNIT = ENV.fetch('DISTANCE_UNIT', 'km') DISTANCE_UNIT = ENV.fetch('DISTANCE_UNIT', 'km').to_sym

View file

@ -2,7 +2,7 @@
settings = { settings = {
timeout: 5, timeout: 5,
units: DISTANCE_UNIT.to_sym, units: DISTANCE_UNIT,
cache: Redis.new, cache: Redis.new,
always_raise: :all, always_raise: :all,
cache_options: { cache_options: {

View file

@ -15,7 +15,7 @@ FactoryBot.define do
connection { 1 } connection { 1 }
vertical_accuracy { 1 } vertical_accuracy { 1 }
accuracy { 1 } accuracy { 1 }
timestamp { 1.year.ago.to_i } timestamp { DateTime.new(2024, 5, 1).to_i + rand(1_000).minutes }
latitude { FFaker::Geolocation.lat } latitude { FFaker::Geolocation.lat }
mode { 1 } mode { 1 }
inrids { 'MyString' } inrids { 'MyString' }

View file

@ -88,9 +88,31 @@ RSpec.describe 'Api::V1::Points', type: :request do
json_response = JSON.parse(response.body) json_response = JSON.parse(response.body)
json_response.each do |point| json_response.each do |point|
expect(point.keys).to eq(%w[latitude longitude timestamp]) expect(point.keys).to eq(%w[id latitude longitude timestamp])
end end
end end
end end
context 'when order param is provided' do
it 'returns points in ascending order' do
get api_v1_points_url(api_key: user.api_key, order: 'asc')
expect(response).to have_http_status(:ok)
json_response = JSON.parse(response.body)
expect(json_response.first['timestamp']).to be < json_response.last['timestamp']
end
it 'returns points in descending order' do
get api_v1_points_url(api_key: user.api_key, order: 'desc')
expect(response).to have_http_status(:ok)
json_response = JSON.parse(response.body)
expect(json_response.first['timestamp']).to be > json_response.last['timestamp']
end
end
end end
end end

View file

@ -22,6 +22,8 @@ RSpec.describe CreateStats do
let!(:point3) { create(:point, user:, import:, latitude: 3, longitude: 4) } let!(:point3) { create(:point, user:, import:, latitude: 3, longitude: 4) }
context 'when units are kilometers' do context 'when units are kilometers' do
before { stub_const('DISTANCE_UNIT', :km) }
it 'creates stats' do it 'creates stats' do
expect { create_stats }.to change { Stat.count }.by(1) expect { create_stats }.to change { Stat.count }.by(1)
end end
@ -29,7 +31,7 @@ RSpec.describe CreateStats do
it 'calculates distance' do it 'calculates distance' do
create_stats create_stats
expect(Stat.last.distance).to eq(563) expect(user.stats.last.distance).to eq(563)
end end
it 'created notifications' do it 'created notifications' do
@ -52,7 +54,7 @@ RSpec.describe CreateStats do
end end
context 'when units are miles' do context 'when units are miles' do
before { stub_const('DISTANCE_UNIT', 'mi') } before { stub_const('DISTANCE_UNIT', :mi) }
it 'creates stats' do it 'creates stats' do
expect { create_stats }.to change { Stat.count }.by(1) expect { create_stats }.to change { Stat.count }.by(1)
@ -61,7 +63,7 @@ RSpec.describe CreateStats do
it 'calculates distance' do it 'calculates distance' do
create_stats create_stats
expect(Stat.last.distance).to eq(349) expect(user.stats.last.distance).to eq(349)
end end
it 'created notifications' do it 'created notifications' do

View file

@ -14,6 +14,8 @@ describe 'Points API', type: :request do
description: 'End date (i.e. 2024-02-03T13:00:03Z or 2024-02-03)' description: 'End date (i.e. 2024-02-03T13:00:03Z or 2024-02-03)'
parameter name: :page, in: :query, type: :integer, required: false, description: 'Page number' parameter name: :page, in: :query, type: :integer, required: false, description: 'Page number'
parameter name: :per_page, in: :query, type: :integer, required: false, description: 'Number of points per page' parameter name: :per_page, in: :query, type: :integer, required: false, description: 'Number of points per page'
parameter name: :order, in: :query, type: :string, required: false,
description: 'Order of points, valid values are `asc` or `desc`'
response '200', 'points found' do response '200', 'points found' do
schema type: :array, schema type: :array,
items: { items: {

View file

@ -346,6 +346,12 @@ paths:
description: Number of points per page description: Number of points per page
schema: schema:
type: integer type: integer
- name: order
in: query
required: false
description: Order of points, valid values are `asc` or `desc`
schema:
type: string
responses: responses:
'200': '200':
description: points found description: points found