diff --git a/app/controllers/stats_controller.rb b/app/controllers/stats_controller.rb index 809f1d98..2305a44e 100644 --- a/app/controllers/stats_controller.rb +++ b/app/controllers/stats_controller.rb @@ -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 diff --git a/app/jobs/stats/calculating_job.rb b/app/jobs/stats/calculating_job.rb index f7a5bc73..02da4d5e 100644 --- a/app/jobs/stats/calculating_job.rb +++ b/app/jobs/stats/calculating_job.rb @@ -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 diff --git a/app/models/stat.rb b/app/models/stat.rb index 9376c991..6b2d56dd 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -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 diff --git a/app/services/countries_and_cities.rb b/app/services/countries_and_cities.rb index efb36511..b08f2e9a 100644 --- a/app/services/countries_and_cities.rb +++ b/app/services/countries_and_cities.rb @@ -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 diff --git a/spec/models/stat_spec.rb b/spec/models/stat_spec.rb index ae65afd2..af8873b6 100644 --- a/spec/models/stat_spec.rb +++ b/spec/models/stat_spec.rb @@ -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 } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 26bd7e27..a1059d0a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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 diff --git a/spec/requests/stats_spec.rb b/spec/requests/stats_spec.rb index 40c19823..224cb3b5 100644 --- a/spec/requests/stats_spec.rb +++ b/spec/requests/stats_spec.rb @@ -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 diff --git a/spec/services/countries_and_cities_spec.rb b/spec/services/countries_and_cities_spec.rb index 99e7664f..636823e5 100644 --- a/spec/services/countries_and_cities_spec.rb +++ b/spec/services/countries_and_cities_spec.rb @@ -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 diff --git a/spec/swagger/api/v1/countries/visited_cities_spec.rb b/spec/swagger/api/v1/countries/visited_cities_spec.rb new file mode 100644 index 00000000..5e59dcd1 --- /dev/null +++ b/spec/swagger/api/v1/countries/visited_cities_spec.rb @@ -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 diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index 9e6a57dd..07235bd6 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -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