Refactor code a bit and add some specs

This commit is contained in:
Eugene Burmakin 2024-12-16 15:10:46 +01:00
parent 41dfbfc1f4
commit d9930521c9
10 changed files with 220 additions and 82 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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 }

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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