mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Add stats API endpoint and serializer
This commit is contained in:
parent
587594521d
commit
7ed7f9795b
15 changed files with 328 additions and 7 deletions
File diff suppressed because one or more lines are too long
10
app/controllers/api/v1/stats_controller.rb
Normal file
10
app/controllers/api/v1/stats_controller.rb
Normal 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
|
||||||
|
|
@ -17,8 +17,8 @@ class Point < ApplicationRecord
|
||||||
}, _suffix: true
|
}, _suffix: true
|
||||||
enum connection: { mobile: 0, wifi: 1, offline: 2 }, _suffix: true
|
enum connection: { mobile: 0, wifi: 1, offline: 2 }, _suffix: true
|
||||||
|
|
||||||
scope :reverse_geocoded, -> { where.not(city: nil, country: nil) }
|
scope :reverse_geocoded, -> { where.not(geodata: {}) }
|
||||||
scope :not_reverse_geocoded, -> { where(city: nil, country: nil) }
|
scope :not_reverse_geocoded, -> { where(geodata: {}) }
|
||||||
|
|
||||||
after_create :async_reverse_geocode
|
after_create :async_reverse_geocode
|
||||||
|
|
||||||
|
|
|
||||||
54
app/serializers/stats_serializer.rb
Normal file
54
app/serializers/stats_serializer.rb
Normal 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
|
||||||
|
|
@ -57,6 +57,7 @@ Rails.application.routes.draw do
|
||||||
namespace :v1 do
|
namespace :v1 do
|
||||||
resources :areas, only: %i[index create update destroy]
|
resources :areas, only: %i[index create update destroy]
|
||||||
resources :points, only: %i[index destroy]
|
resources :points, only: %i[index destroy]
|
||||||
|
resources :stats, only: :index
|
||||||
|
|
||||||
namespace :overland do
|
namespace :overland do
|
||||||
resources :batches, only: :create
|
resources :batches, only: :create
|
||||||
|
|
|
||||||
|
|
@ -26,5 +26,29 @@ FactoryBot.define do
|
||||||
city { nil }
|
city { nil }
|
||||||
country { nil }
|
country { nil }
|
||||||
user
|
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
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ FactoryBot.define do
|
||||||
year { 1 }
|
year { 1 }
|
||||||
month { 1 }
|
month { 1 }
|
||||||
distance { 1 }
|
distance { 1 }
|
||||||
|
user
|
||||||
toponyms do
|
toponyms do
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ RSpec.describe Point, type: :model do
|
||||||
|
|
||||||
describe 'scopes' do
|
describe 'scopes' do
|
||||||
describe '.reverse_geocoded' 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) }
|
let(:point_without_address) { create(:point, city: nil, country: nil) }
|
||||||
|
|
||||||
it 'returns points with reverse geocoded address' do
|
it 'returns points with reverse geocoded address' do
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ abort('The Rails environment is running in production mode!') if Rails.env.produ
|
||||||
require 'rspec/rails'
|
require 'rspec/rails'
|
||||||
require 'rswag/specs'
|
require 'rswag/specs'
|
||||||
require 'sidekiq/testing'
|
require 'sidekiq/testing'
|
||||||
|
require 'super_diff/rspec-rails'
|
||||||
|
|
||||||
require 'rake'
|
require 'rake'
|
||||||
|
|
||||||
|
|
|
||||||
7
spec/requests/api/v1/stats_spec.rb
Normal file
7
spec/requests/api/v1/stats_spec.rb
Normal 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
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rails_helper'
|
require 'rails_helper'
|
||||||
|
|
||||||
RSpec.describe ExportSerializer do
|
RSpec.describe ExportSerializer do
|
||||||
|
|
|
||||||
70
spec/serializers/stats_serializer_spec.rb
Normal file
70
spec/serializers/stats_serializer_spec.rb
Normal 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
|
||||||
|
|
@ -32,11 +32,11 @@ RSpec.describe ReverseGeocoding::FetchData do
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when point has city and country' do
|
context 'when point has city and country' do
|
||||||
let(:point) { create(:point, city: 'City', country: 'Country') }
|
let(:point) { create(:point, :with_geodata) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
allow(Geocoder).to receive(:search).and_return(
|
allow(Geocoder).to receive(:search).and_return(
|
||||||
[double(city: 'Another city', country: 'Some country')]
|
[double(geodata: { 'address' => 'Address' }, city: 'City', country: 'Country')]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
68
spec/swagger/api/v1/stats_controller_spec.rb
Normal file
68
spec/swagger/api/v1/stats_controller_spec.rb
Normal 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
|
||||||
|
|
@ -415,6 +415,89 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: point deleted
|
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:
|
servers:
|
||||||
- url: http://{defaultHost}
|
- url: http://{defaultHost}
|
||||||
variables:
|
variables:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue