mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -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
|
||||
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
|
||||
|
||||
|
|
|
|||
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
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ FactoryBot.define do
|
|||
year { 1 }
|
||||
month { 1 }
|
||||
distance { 1 }
|
||||
user
|
||||
toponyms do
|
||||
[
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
||||
|
|
|
|||
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'
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
|
|
|
|||
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:
|
||||
'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:
|
||||
|
|
|
|||
Loading…
Reference in a new issue