mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Refactor code a bit and add some specs
This commit is contained in:
parent
41dfbfc1f4
commit
d9930521c9
10 changed files with 220 additions and 82 deletions
|
|
@ -17,8 +17,10 @@ class StatsController < ApplicationController
|
||||||
|
|
||||||
def update
|
def update
|
||||||
current_user.years_tracked.each do |year|
|
current_user.years_tracked.each do |year|
|
||||||
(1..12).each do |month|
|
year[:months].each do |month|
|
||||||
Stats::CalculatingJob.perform_later(current_user.id, year, month)
|
Stats::CalculatingJob.perform_later(
|
||||||
|
current_user.id, year[:year], Date::ABBR_MONTHNAMES.index(month)
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ class Stats::CalculatingJob < ApplicationJob
|
||||||
def perform(user_id, year, month)
|
def perform(user_id, year, month)
|
||||||
Stats::CalculateMonth.new(user_id, year, month).call
|
Stats::CalculateMonth.new(user_id, year, month).call
|
||||||
|
|
||||||
create_stats_updated_notification(user_id)
|
create_stats_updated_notification(user_id, year, month)
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
create_stats_update_failed_notification(user_id, e)
|
create_stats_update_failed_notification(user_id, e)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -21,20 +21,6 @@ class Stat < ApplicationRecord
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.year_cities_and_countries(year, user)
|
|
||||||
start_at = DateTime.new(year).beginning_of_year
|
|
||||||
end_at = DateTime.new(year).end_of_year
|
|
||||||
|
|
||||||
points = user.tracked_points.without_raw_data.where(timestamp: start_at..end_at)
|
|
||||||
|
|
||||||
data = CountriesAndCities.new(points).call
|
|
||||||
|
|
||||||
{
|
|
||||||
countries: data.map { _1[:country] }.uniq.count,
|
|
||||||
cities: data.sum { _1[:cities].count }
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def points
|
def points
|
||||||
user.tracked_points
|
user.tracked_points
|
||||||
.without_raw_data
|
.without_raw_data
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class CountriesAndCities
|
class CountriesAndCities
|
||||||
CityStats = Struct.new(:points, :last_timestamp, :stayed_for, keyword_init: true)
|
MIN_MINUTES_SPENT_IN_CITY = 30 # You can adjust this value as needed
|
||||||
|
|
||||||
CountryData = Struct.new(:country, :cities, keyword_init: true)
|
CountryData = Struct.new(:country, :cities, keyword_init: true)
|
||||||
CityData = Struct.new(:city, :points, :timestamp, :stayed_for, keyword_init: true)
|
CityData = Struct.new(:city, :points, :timestamp, :stayed_for, keyword_init: true)
|
||||||
|
|
||||||
|
|
@ -24,19 +25,28 @@ class CountriesAndCities
|
||||||
def process_country_points(country_points)
|
def process_country_points(country_points)
|
||||||
country_points
|
country_points
|
||||||
.group_by(&:city)
|
.group_by(&:city)
|
||||||
.transform_values do |city_points|
|
.transform_values { |city_points| create_city_data_if_valid(city_points) }
|
||||||
timestamps = city_points.map(&:timestamp)
|
|
||||||
build_city_data(city_points.first.city, city_points.size, timestamps)
|
|
||||||
end
|
|
||||||
.values
|
.values
|
||||||
|
.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_city_data(city, points_count, timestamps)
|
def create_city_data_if_valid(city_points)
|
||||||
|
timestamps = city_points.pluck(:timestamp)
|
||||||
|
duration = calculate_duration_in_minutes(timestamps)
|
||||||
|
city = city_points.first.city
|
||||||
|
points_count = city_points.size
|
||||||
|
|
||||||
|
build_city_data(city, points_count, timestamps, duration)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_city_data(city, points_count, timestamps, duration)
|
||||||
|
return nil if duration < MIN_MINUTES_SPENT_IN_CITY
|
||||||
|
|
||||||
CityData.new(
|
CityData.new(
|
||||||
city: city,
|
city: city,
|
||||||
points: points_count,
|
points: points_count,
|
||||||
timestamp: timestamps.max,
|
timestamp: timestamps.max,
|
||||||
stayed_for: calculate_duration_in_minutes(timestamps)
|
stayed_for: duration
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,44 +13,6 @@ RSpec.describe Stat, type: :model do
|
||||||
let(:year) { 2021 }
|
let(:year) { 2021 }
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
describe '.year_cities_and_countries' do
|
|
||||||
subject { described_class.year_cities_and_countries(year, user) }
|
|
||||||
|
|
||||||
let(:timestamp) { DateTime.new(year, 1, 1, 0, 0, 0) }
|
|
||||||
|
|
||||||
before do
|
|
||||||
stub_const('MIN_MINUTES_SPENT_IN_CITY', 60)
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there are points' do
|
|
||||||
let!(:points) do
|
|
||||||
[
|
|
||||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp:),
|
|
||||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 10.minutes),
|
|
||||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 20.minutes),
|
|
||||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 30.minutes),
|
|
||||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 40.minutes),
|
|
||||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 50.minutes),
|
|
||||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 60.minutes),
|
|
||||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 70.minutes),
|
|
||||||
create(:point, user:, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 80.minutes),
|
|
||||||
create(:point, user:, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 90.minutes)
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'returns countries and cities' do
|
|
||||||
# User spent only 20 minutes in Brugges, so it should not be included
|
|
||||||
expect(subject).to eq(countries: 2, cities: 1)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when there are no points' do
|
|
||||||
it 'returns countries and cities' do
|
|
||||||
expect(subject).to eq(countries: 0, cities: 0)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#distance_by_day' do
|
describe '#distance_by_day' do
|
||||||
subject { stat.distance_by_day }
|
subject { stat.distance_by_day }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -117,10 +117,8 @@ RSpec.describe User, type: :model do
|
||||||
describe '#years_tracked' do
|
describe '#years_tracked' do
|
||||||
let!(:points) { create_list(:point, 3, user:, timestamp: DateTime.new(2024, 1, 1, 5, 0, 0)) }
|
let!(:points) { create_list(:point, 3, user:, timestamp: DateTime.new(2024, 1, 1, 5, 0, 0)) }
|
||||||
|
|
||||||
subject { user.years_tracked }
|
|
||||||
|
|
||||||
it 'returns years tracked' do
|
it 'returns years tracked' do
|
||||||
expect(subject).to eq([2024])
|
expect(user.years_tracked).to eq([{ year: 2024, months: ['Jan'] }])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -55,13 +55,13 @@ RSpec.describe '/stats', type: :request do
|
||||||
let(:stat) { create(:stat, user:, year: 2024) }
|
let(:stat) { create(:stat, user:, year: 2024) }
|
||||||
|
|
||||||
it 'enqueues Stats::CalculatingJob for each tracked year and month' do
|
it 'enqueues Stats::CalculatingJob for each tracked year and month' do
|
||||||
allow(user).to receive(:years_tracked).and_return([2024])
|
allow(user).to receive(:years_tracked).and_return([{ year: 2024, months: %w[Jan Feb] }])
|
||||||
|
|
||||||
post stats_url
|
post stats_url
|
||||||
|
|
||||||
(1..12).each do |month|
|
expect(Stats::CalculatingJob).to have_been_enqueued.with(user.id, 2024, 1)
|
||||||
expect(Stats::CalculatingJob).to have_been_enqueued.with(user.id, 2024, month)
|
expect(Stats::CalculatingJob).to have_been_enqueued.with(user.id, 2024, 2)
|
||||||
end
|
expect(Stats::CalculatingJob).to_not have_been_enqueued.with(user.id, 2024, 3)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -36,13 +36,18 @@ RSpec.describe CountriesAndCities do
|
||||||
it 'returns countries and cities' do
|
it 'returns countries and cities' do
|
||||||
expect(countries_and_cities).to eq(
|
expect(countries_and_cities).to eq(
|
||||||
[
|
[
|
||||||
{
|
CountriesAndCities::CountryData.new(
|
||||||
cities: [{ city: 'Berlin', points: 8, timestamp: 1609463400, stayed_for: 70 }],
|
country: 'Germany',
|
||||||
country: 'Germany'
|
cities: [
|
||||||
},
|
CountriesAndCities::CityData.new(
|
||||||
{
|
city: 'Berlin', points: 8, timestamp: 1_609_463_400, stayed_for: 70
|
||||||
cities: [], country: 'Belgium'
|
)
|
||||||
}
|
]
|
||||||
|
),
|
||||||
|
CountriesAndCities::CountryData.new(
|
||||||
|
country: 'Belgium',
|
||||||
|
cities: []
|
||||||
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
@ -62,12 +67,14 @@ RSpec.describe CountriesAndCities do
|
||||||
it 'returns countries and cities' do
|
it 'returns countries and cities' do
|
||||||
expect(countries_and_cities).to eq(
|
expect(countries_and_cities).to eq(
|
||||||
[
|
[
|
||||||
{
|
CountriesAndCities::CountryData.new(
|
||||||
cities: [], country: 'Germany'
|
country: 'Germany',
|
||||||
},
|
cities: []
|
||||||
{
|
),
|
||||||
cities: [], country: 'Belgium'
|
CountriesAndCities::CountryData.new(
|
||||||
}
|
country: 'Belgium',
|
||||||
|
cities: []
|
||||||
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
|
||||||
95
spec/swagger/api/v1/countries/visited_cities_spec.rb
Normal file
95
spec/swagger/api/v1/countries/visited_cities_spec.rb
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
# spec/swagger/api/v1/countries/visited_cities_controller_spec.rb
|
||||||
|
require 'swagger_helper'
|
||||||
|
|
||||||
|
RSpec.describe 'Api::V1::Countries::VisitedCities', type: :request do
|
||||||
|
path '/api/v1/countries/visited_cities' do
|
||||||
|
get 'Get visited cities by date range' do
|
||||||
|
tags 'Countries'
|
||||||
|
description 'Returns a list of visited cities and countries based on tracked points within the specified date range'
|
||||||
|
produces 'application/json'
|
||||||
|
|
||||||
|
parameter name: :api_key, in: :query, type: :string, required: true
|
||||||
|
parameter name: :start_at,
|
||||||
|
in: :query,
|
||||||
|
type: :string,
|
||||||
|
format: 'date-time',
|
||||||
|
required: true,
|
||||||
|
description: 'Start date and time for the range (ISO 8601 format)',
|
||||||
|
example: '2023-01-01T00:00:00Z'
|
||||||
|
|
||||||
|
parameter name: :end_at,
|
||||||
|
in: :query,
|
||||||
|
type: :string,
|
||||||
|
format: 'date-time',
|
||||||
|
required: true,
|
||||||
|
description: 'End date and time for the range (ISO 8601 format)',
|
||||||
|
example: '2023-12-31T23:59:59Z'
|
||||||
|
|
||||||
|
response '200', 'cities found' do
|
||||||
|
schema type: :object,
|
||||||
|
properties: {
|
||||||
|
data: {
|
||||||
|
type: :array,
|
||||||
|
description: 'Array of countries and their visited cities',
|
||||||
|
items: {
|
||||||
|
type: :object,
|
||||||
|
properties: {
|
||||||
|
country: {
|
||||||
|
type: :string,
|
||||||
|
example: 'Germany'
|
||||||
|
},
|
||||||
|
cities: {
|
||||||
|
type: :array,
|
||||||
|
items: {
|
||||||
|
type: :object,
|
||||||
|
properties: {
|
||||||
|
city: {
|
||||||
|
type: :string,
|
||||||
|
example: 'Berlin'
|
||||||
|
},
|
||||||
|
points: {
|
||||||
|
type: :integer,
|
||||||
|
example: 4394,
|
||||||
|
description: 'Number of points in the city'
|
||||||
|
},
|
||||||
|
timestamp: {
|
||||||
|
type: :integer,
|
||||||
|
example: 1_724_868_369,
|
||||||
|
description: 'Timestamp of the last point in the city in seconds since Unix epoch'
|
||||||
|
},
|
||||||
|
stayed_for: {
|
||||||
|
type: :integer,
|
||||||
|
example: 24_490,
|
||||||
|
description: 'Number of minutes the user stayed in the city'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let(:start_at) { '2023-01-01T00:00:00Z' }
|
||||||
|
let(:end_at) { '2023-12-31T23:59:59Z' }
|
||||||
|
let(:api_key) { create(:user).api_key }
|
||||||
|
run_test!
|
||||||
|
end
|
||||||
|
|
||||||
|
response '400', 'bad request - missing parameters' do
|
||||||
|
schema type: :object,
|
||||||
|
properties: {
|
||||||
|
error: {
|
||||||
|
type: :string,
|
||||||
|
example: 'Missing required parameters: start_at, end_at'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let(:start_at) { nil }
|
||||||
|
let(:end_at) { nil }
|
||||||
|
let(:api_key) { create(:user).api_key }
|
||||||
|
run_test!
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -106,6 +106,84 @@ paths:
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: area deleted
|
description: area deleted
|
||||||
|
"/api/v1/countries/visited_cities":
|
||||||
|
get:
|
||||||
|
summary: Get visited cities by date range
|
||||||
|
tags:
|
||||||
|
- Countries
|
||||||
|
description: Returns a list of visited cities and countries based on tracked
|
||||||
|
points within the specified date range
|
||||||
|
parameters:
|
||||||
|
- name: api_key
|
||||||
|
in: query
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: start_at
|
||||||
|
in: query
|
||||||
|
format: date-time
|
||||||
|
required: true
|
||||||
|
description: Start date and time for the range (ISO 8601 format)
|
||||||
|
example: '2023-01-01T00:00:00Z'
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
- name: end_at
|
||||||
|
in: query
|
||||||
|
format: date-time
|
||||||
|
required: true
|
||||||
|
description: End date and time for the range (ISO 8601 format)
|
||||||
|
example: '2023-12-31T23:59:59Z'
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: cities found
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
data:
|
||||||
|
type: array
|
||||||
|
description: Array of countries and their visited cities
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
country:
|
||||||
|
type: string
|
||||||
|
example: Germany
|
||||||
|
cities:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
city:
|
||||||
|
type: string
|
||||||
|
example: Berlin
|
||||||
|
points:
|
||||||
|
type: integer
|
||||||
|
example: 4394
|
||||||
|
description: Number of points in the city
|
||||||
|
timestamp:
|
||||||
|
type: integer
|
||||||
|
example: 1724868369
|
||||||
|
description: Timestamp of the last point in the city
|
||||||
|
in seconds since Unix epoch
|
||||||
|
stayed_for:
|
||||||
|
type: integer
|
||||||
|
example: 24490
|
||||||
|
description: Number of minutes the user stayed in
|
||||||
|
the city
|
||||||
|
'400':
|
||||||
|
description: bad request - missing parameters
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
error:
|
||||||
|
type: string
|
||||||
|
example: 'Missing required parameters: start_at, end_at'
|
||||||
"/api/v1/health":
|
"/api/v1/health":
|
||||||
get:
|
get:
|
||||||
summary: Retrieves application status
|
summary: Retrieves application status
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue