mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01:39 -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
|
||||
current_user.years_tracked.each do |year|
|
||||
(1..12).each do |month|
|
||||
Stats::CalculatingJob.perform_later(current_user.id, year, month)
|
||||
year[:months].each do |month|
|
||||
Stats::CalculatingJob.perform_later(
|
||||
current_user.id, year[:year], Date::ABBR_MONTHNAMES.index(month)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ class Stats::CalculatingJob < ApplicationJob
|
|||
def perform(user_id, year, month)
|
||||
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
|
||||
create_stats_update_failed_notification(user_id, e)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,20 +21,6 @@ class Stat < ApplicationRecord
|
|||
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
|
||||
user.tracked_points
|
||||
.without_raw_data
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
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)
|
||||
CityData = Struct.new(:city, :points, :timestamp, :stayed_for, keyword_init: true)
|
||||
|
||||
|
|
@ -24,19 +25,28 @@ class CountriesAndCities
|
|||
def process_country_points(country_points)
|
||||
country_points
|
||||
.group_by(&:city)
|
||||
.transform_values do |city_points|
|
||||
timestamps = city_points.map(&:timestamp)
|
||||
build_city_data(city_points.first.city, city_points.size, timestamps)
|
||||
end
|
||||
.transform_values { |city_points| create_city_data_if_valid(city_points) }
|
||||
.values
|
||||
.compact
|
||||
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(
|
||||
city: city,
|
||||
points: points_count,
|
||||
timestamp: timestamps.max,
|
||||
stayed_for: calculate_duration_in_minutes(timestamps)
|
||||
stayed_for: duration
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -13,44 +13,6 @@ RSpec.describe Stat, type: :model do
|
|||
let(:year) { 2021 }
|
||||
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
|
||||
subject { stat.distance_by_day }
|
||||
|
||||
|
|
|
|||
|
|
@ -117,10 +117,8 @@ RSpec.describe User, type: :model do
|
|||
describe '#years_tracked' do
|
||||
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
|
||||
expect(subject).to eq([2024])
|
||||
expect(user.years_tracked).to eq([{ year: 2024, months: ['Jan'] }])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -55,13 +55,13 @@ RSpec.describe '/stats', type: :request do
|
|||
let(:stat) { create(:stat, user:, year: 2024) }
|
||||
|
||||
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
|
||||
|
||||
(1..12).each do |month|
|
||||
expect(Stats::CalculatingJob).to have_been_enqueued.with(user.id, 2024, month)
|
||||
end
|
||||
expect(Stats::CalculatingJob).to have_been_enqueued.with(user.id, 2024, 1)
|
||||
expect(Stats::CalculatingJob).to have_been_enqueued.with(user.id, 2024, 2)
|
||||
expect(Stats::CalculatingJob).to_not have_been_enqueued.with(user.id, 2024, 3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -36,13 +36,18 @@ RSpec.describe CountriesAndCities do
|
|||
it 'returns countries and cities' do
|
||||
expect(countries_and_cities).to eq(
|
||||
[
|
||||
{
|
||||
cities: [{ city: 'Berlin', points: 8, timestamp: 1609463400, stayed_for: 70 }],
|
||||
country: 'Germany'
|
||||
},
|
||||
{
|
||||
cities: [], country: 'Belgium'
|
||||
}
|
||||
CountriesAndCities::CountryData.new(
|
||||
country: 'Germany',
|
||||
cities: [
|
||||
CountriesAndCities::CityData.new(
|
||||
city: 'Berlin', points: 8, timestamp: 1_609_463_400, stayed_for: 70
|
||||
)
|
||||
]
|
||||
),
|
||||
CountriesAndCities::CountryData.new(
|
||||
country: 'Belgium',
|
||||
cities: []
|
||||
)
|
||||
]
|
||||
)
|
||||
end
|
||||
|
|
@ -62,12 +67,14 @@ RSpec.describe CountriesAndCities do
|
|||
it 'returns countries and cities' do
|
||||
expect(countries_and_cities).to eq(
|
||||
[
|
||||
{
|
||||
cities: [], country: 'Germany'
|
||||
},
|
||||
{
|
||||
cities: [], country: 'Belgium'
|
||||
}
|
||||
CountriesAndCities::CountryData.new(
|
||||
country: 'Germany',
|
||||
cities: []
|
||||
),
|
||||
CountriesAndCities::CountryData.new(
|
||||
country: 'Belgium',
|
||||
cities: []
|
||||
)
|
||||
]
|
||||
)
|
||||
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:
|
||||
'200':
|
||||
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":
|
||||
get:
|
||||
summary: Retrieves application status
|
||||
|
|
|
|||
Loading…
Reference in a new issue