Add stats API endpoint and serializer

This commit is contained in:
Eugene Burmakin 2024-08-20 20:14:17 +02:00
parent 587594521d
commit 7ed7f9795b
15 changed files with 328 additions and 7 deletions

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,10 @@
# frozen_string_literal: true
class Api::V1::StatsController < ApplicationController
skip_forgery_protection
before_action :authenticate_api_key
def index
render json: StatsSerializer.new(current_api_user).call
end
end

View file

@ -17,8 +17,8 @@ class Point < ApplicationRecord
}, _suffix: true
enum connection: { mobile: 0, wifi: 1, offline: 2 }, _suffix: true
scope :reverse_geocoded, -> { where.not(city: nil, country: nil) }
scope :not_reverse_geocoded, -> { where(city: nil, country: nil) }
scope :reverse_geocoded, -> { where.not(geodata: {}) }
scope :not_reverse_geocoded, -> { where(geodata: {}) }
after_create :async_reverse_geocode

View file

@ -0,0 +1,54 @@
# frozen_string_literal: true
class StatsSerializer
attr_reader :user
def initialize(user)
@user = user
end
def call
{
totalDistanceKm: total_distance,
totalPointsTracked: user.tracked_points.count,
totalReverseGeocodedPoints: reverse_geocoded_points,
totalCountriesVisited: user.countries_visited.count,
totalCitiesVisited: user.cities_visited.count,
yearlyStats: yearly_stats
}.to_json
end
private
def total_distance
user.stats.sum(:distance)
end
def reverse_geocoded_points
user.tracked_points.reverse_geocoded.count
end
def yearly_stats
user.stats.group_by(&:year).sort.reverse.map do |year, stats|
{
year:,
totalDistanceKm: stats.sum(&:distance),
totalCountriesVisited: user.countries_visited.count,
totalCitiesVisited: user.cities_visited.count,
monthlyDistanceKm: monthly_distance(year, stats)
}
end
end
def monthly_distance(year, stats)
months = {}
(1..12).each { |month| months[Date::MONTHNAMES[month]&.downcase] = distance(month, year, stats) }
months
end
def distance(month, year, stats)
stats.find { _1.month == month && _1.year == year }&.distance.to_i
end
end

View file

@ -57,6 +57,7 @@ Rails.application.routes.draw do
namespace :v1 do
resources :areas, only: %i[index create update destroy]
resources :points, only: %i[index destroy]
resources :stats, only: :index
namespace :overland do
resources :batches, only: :create

View file

@ -26,5 +26,29 @@ FactoryBot.define do
city { nil }
country { nil }
user
trait :with_geodata do
geodata do
{
'type' => 'Feature',
'geometry' => { 'type' => 'Point', 'coordinates' => [37.6177036, 55.755847] },
'properties' => {
'city' => 'Moscow',
'name' => 'Kilometre zero',
'type' => 'house',
'state' => 'Moscow',
'osm_id' => 583_204_619,
'street' => 'Манежная площадь',
'country' => 'Russia',
'osm_key' => 'tourism',
'district' => 'Tverskoy',
'osm_type' => 'N',
'postcode' => '103265',
'osm_value' => 'attraction',
'countrycode' => 'RU'
}
}
end
end
end
end

View file

@ -5,6 +5,7 @@ FactoryBot.define do
year { 1 }
month { 1 }
distance { 1 }
user
toponyms do
[
{

View file

@ -16,7 +16,7 @@ RSpec.describe Point, type: :model do
describe 'scopes' do
describe '.reverse_geocoded' do
let(:point) { create(:point, country: 'Country', city: 'City') }
let(:point) { create(:point, :with_geodata) }
let(:point_without_address) { create(:point, city: nil, country: nil) }
it 'returns points with reverse geocoded address' do

View file

@ -8,6 +8,7 @@ abort('The Rails environment is running in production mode!') if Rails.env.produ
require 'rspec/rails'
require 'rswag/specs'
require 'sidekiq/testing'
require 'super_diff/rspec-rails'
require 'rake'

View file

@ -0,0 +1,7 @@
require 'rails_helper'
RSpec.describe "Api::V1::Stats", type: :request do
describe "GET /index" do
pending "add some examples (or delete) #{__FILE__}"
end
end

View file

@ -1,3 +1,5 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe ExportSerializer do

View file

@ -0,0 +1,70 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe StatsSerializer do
describe '#call' do
subject(:serializer) { described_class.new(user).call }
let!(:user) { create(:user) }
let!(:stats_in_2020) { create_list(:stat, 12, year: 2020, user:) }
let!(:stats_in_2021) { create_list(:stat, 12, year: 2021, user:) }
let!(:points_in_2020) { create_list(:point, 85, :with_geodata, timestamp: Time.zone.local(2020), user:) }
let!(:points_in_2021) { create_list(:point, 95, timestamp: Time.zone.local(2021), user:) }
let(:expected_json) do
{
"totalDistanceKm": stats_in_2020.map(&:distance).sum + stats_in_2021.map(&:distance).sum,
"totalPointsTracked": points_in_2020.count + points_in_2021.count,
"totalReverseGeocodedPoints": points_in_2020.count,
"totalCountriesVisited": 1,
"totalCitiesVisited": 1,
"yearlyStats": [
{
"year": 2021,
"totalDistanceKm": 12,
"totalCountriesVisited": 1,
"totalCitiesVisited": 1,
"monthlyDistanceKm": {
"january": 1,
"february": 0,
"march": 0,
"april": 0,
"may": 0,
"june": 0,
"july": 0,
"august": 0,
"september": 0,
"october": 0,
"november": 0,
"december": 0
}
},
{
"year": 2020,
"totalDistanceKm": 12,
"totalCountriesVisited": 1,
"totalCitiesVisited": 1,
"monthlyDistanceKm": {
"january": 1,
"february": 0,
"march": 0,
"april": 0,
"may": 0,
"june": 0,
"july": 0,
"august": 0,
"september": 0,
"october": 0,
"november": 0,
"december": 0
}
}
]
}.to_json
end
it 'returns the expected JSON' do
expect(serializer).to eq(expected_json)
end
end
end

View file

@ -32,11 +32,11 @@ RSpec.describe ReverseGeocoding::FetchData do
end
context 'when point has city and country' do
let(:point) { create(:point, city: 'City', country: 'Country') }
let(:point) { create(:point, :with_geodata) }
before do
allow(Geocoder).to receive(:search).and_return(
[double(city: 'Another city', country: 'Some country')]
[double(geodata: { 'address' => 'Address' }, city: 'City', country: 'Country')]
)
end

View file

@ -0,0 +1,68 @@
# frozen_string_literal: true
require 'swagger_helper'
describe 'Stats API', type: :request do
path '/api/v1/stats' do
get 'Retrieves all stats' do
tags 'Stats'
produces 'application/json'
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
response '200', 'stats found' do
schema type: :object,
properties: {
totalDistanceKm: { type: :number },
totalPointsTracked: { type: :number },
totalReverseGeocodedPoints: { type: :number },
totalCountriesVisited: { type: :number },
totalCitiesVisited: { type: :number },
yearlyStats: {
type: :array,
items: {
type: :object,
properties: {
year: { type: :integer },
totalDistanceKm: { type: :number },
totalCountriesVisited: { type: :number },
totalCitiesVisited: { type: :number },
monthlyDistanceKm: {
type: :object,
properties: {
january: { type: :number },
february: { type: :number },
march: { type: :number },
april: { type: :number },
may: { type: :number },
june: { type: :number },
july: { type: :number },
august: { type: :number },
september: { type: :number },
october: { type: :number },
november: { type: :number },
december: { type: :number }
}
}
},
required: %w[
year totalDistanceKm totalCountriesVisited totalCitiesVisited monthlyDistanceKm
]
}
}
},
required: %w[
totalDistanceKm totalPointsTracked totalReverseGeocodedPoints totalCountriesVisited
totalCitiesVisited yearlyStats
]
let!(:user) { create(:user) }
let!(:stats_in_2020) { create_list(:stat, 12, year: 2020, user:) }
let!(:stats_in_2021) { create_list(:stat, 12, year: 2021, user:) }
let!(:points_in_2020) { create_list(:point, 85, :with_geodata, timestamp: Time.zone.local(2020), user:) }
let!(:points_in_2021) { create_list(:point, 95, timestamp: Time.zone.local(2021), user:) }
let(:api_key) { user.api_key }
run_test!
end
end
end
end

View file

@ -415,6 +415,89 @@ paths:
responses:
'200':
description: point deleted
"/api/v1/stats":
get:
summary: Retrieves all stats
tags:
- Stats
parameters:
- name: api_key
in: query
required: true
description: API Key
schema:
type: string
responses:
'200':
description: stats found
content:
application/json:
schema:
type: object
properties:
totalDistanceKm:
type: number
totalPointsTracked:
type: number
totalReverseGeocodedPoints:
type: number
totalCountriesVisited:
type: number
totalCitiesVisited:
type: number
yearlyStats:
type: array
items:
type: object
properties:
year:
type: integer
totalDistanceKm:
type: number
totalCountriesVisited:
type: number
totalCitiesVisited:
type: number
monthlyDistanceKm:
type: object
properties:
january:
type: number
february:
type: number
march:
type: number
april:
type: number
may:
type: number
june:
type: number
july:
type: number
august:
type: number
september:
type: number
october:
type: number
november:
type: number
december:
type: number
required:
- year
- totalDistanceKm
- totalCountriesVisited
- totalCitiesVisited
- monthlyDistanceKm
required:
- totalDistanceKm
- totalPointsTracked
- totalReverseGeocodedPoints
- totalCountriesVisited
- totalCitiesVisited
- yearlyStats
servers:
- url: http://{defaultHost}
variables: