diff --git a/app/models/stat.rb b/app/models/stat.rb index 5250255a..48d8e295 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -5,20 +5,16 @@ class Stat < ApplicationRecord belongs_to :user - def timespan - DateTime.new(year, month).beginning_of_month..DateTime.new(year, month).end_of_month - end - def distance_by_day timespan.to_a.map.with_index(1) do |day, index| beginning_of_day = day.beginning_of_day.to_i end_of_day = day.end_of_day.to_i - data = { day: index, distance: 0 } - # We have to filter by user as well points = Point.where(timestamp: beginning_of_day..end_of_day) + data = { day: index, distance: 0 } + points.each_cons(2) do |point1, point2| distance = Geocoder::Calculations.distance_between( [point1.latitude, point1.longitude], [point2.latitude, point2.longitude] @@ -49,12 +45,18 @@ class Stat < ApplicationRecord data = CountriesAndCities.new(points).call - { countries: data.count, cities: data.sum { |country| country[:cities].count } } + { countries: data.map { _1[:country] }.uniq.count, cities: data.sum { |country| country[:cities].count } } end def self.years - starting_year = pluck(:year).uniq.min || Time.current.year + starting_year = select(:year).min&.year || Time.current.year (starting_year..Time.current.year).to_a.reverse end + + private + + def timespan + DateTime.new(year, month).beginning_of_month..DateTime.new(year, month).end_of_month + end end diff --git a/spec/factories/imports.rb b/spec/factories/imports.rb index 817d2913..65b0fccf 100644 --- a/spec/factories/imports.rb +++ b/spec/factories/imports.rb @@ -1,6 +1,7 @@ FactoryBot.define do factory :import do - user_id { "" } + user + name { 'APRIL_2013.json' } source { 1 } end end diff --git a/spec/factories/points.rb b/spec/factories/points.rb index 4faefff9..ab04f8cd 100644 --- a/spec/factories/points.rb +++ b/spec/factories/points.rb @@ -21,7 +21,7 @@ FactoryBot.define do raw_data { "" } tracker_id { "MyString" } import_id { "" } - city { "MyString" } - country { "MyString" } + city { nil } + country { nil } end end diff --git a/spec/jobs/reverse_geocoding_job_spec.rb b/spec/jobs/reverse_geocoding_job_spec.rb new file mode 100644 index 00000000..b180f27b --- /dev/null +++ b/spec/jobs/reverse_geocoding_job_spec.rb @@ -0,0 +1,61 @@ +require 'rails_helper' + +RSpec.describe ReverseGeocodingJob, type: :job do + describe '#perform' do + subject(:perform) { described_class.new.perform(point.id) } + + let(:point) { create(:point) } + + before do + allow(Geocoder).to receive(:search).and_return([double(city: 'City', country: 'Country')]) + end + + context 'when REVERSE_GEOCODING_ENABLED is false' do + before { stub_const('REVERSE_GEOCODING_ENABLED', false) } + + it 'does not update point' do + expect { perform }.not_to change { point.reload.city } + end + + it 'does not call Geocoder' do + perform + + expect(Geocoder).not_to have_received(:search) + end + end + + context 'when REVERSE_GEOCODING_ENABLED is true' do + before { stub_const('REVERSE_GEOCODING_ENABLED', true) } + + it 'updates point with city and country' do + expect { perform }.to change { point.reload.city }.from(nil) + end + + it 'calls Geocoder' do + perform + + expect(Geocoder).to have_received(:search).with([point.latitude, point.longitude]) + end + + context 'when point has city and country' do + let(:point) { create(:point, city: 'City', country: 'Country') } + + before do + allow(Geocoder).to receive(:search).and_return( + [double(city: 'Another city', country: 'Some country')] + ) + end + + it 'does not update point' do + expect { perform }.not_to change { point.reload.city } + end + + it 'does not call Geocoder' do + perform + + expect(Geocoder).not_to have_received(:search) + end + end + end + end +end diff --git a/spec/jobs/stat_creating_job_spec.rb b/spec/jobs/stat_creating_job_spec.rb index 11c284fd..ff657893 100644 --- a/spec/jobs/stat_creating_job_spec.rb +++ b/spec/jobs/stat_creating_job_spec.rb @@ -1,5 +1,20 @@ require 'rails_helper' RSpec.describe StatCreatingJob, type: :job do - pending "add some examples to (or delete) #{__FILE__}" + describe '#perform' do + let(:user) { create(:user) } + + subject { described_class.perform_now([user.id]) } + + before do + allow(CreateStats).to receive(:new).and_call_original + allow_any_instance_of(CreateStats).to receive(:call) + end + + it 'creates a stat' do + subject + + expect(CreateStats).to have_received(:new).with([user.id]) + end + end end diff --git a/spec/models/stat_spec.rb b/spec/models/stat_spec.rb index 6280a126..a8bc6b7d 100644 --- a/spec/models/stat_spec.rb +++ b/spec/models/stat_spec.rb @@ -1,6 +1,135 @@ require 'rails_helper' RSpec.describe Stat, type: :model do - it { is_expected.to validate_presence_of(:year) } - it { is_expected.to validate_presence_of(:month) } + describe 'associations' do + it { is_expected.to belong_to(:user) } + it { is_expected.to validate_presence_of(:year) } + it { is_expected.to validate_presence_of(:month) } + end + + describe 'methods' do + let(:year) { 2021 } + + describe '.year_cities_and_countries' do + subject { described_class.year_cities_and_countries(year) } + + before do + stub_const('MINIMUM_POINTS_IN_CITY', 1) + end + + context 'when there are points' do + let!(:points) do + create_list(:point, 3, city: 'City', country: 'Country', timestamp: DateTime.new(year, 1)) + create_list(:point, 2, city: 'Some City', country: 'Another country', timestamp: DateTime.new(year, 2)) + end + + + it 'returns countries and cities' do + expect(subject).to eq(countries: 2, cities: 2) + 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 '.years' do + subject { described_class.years } + + context 'when there are no stats' do + it 'returns years' do + expect(subject).to eq([Time.current.year]) + end + end + + context 'when there are stats' do + let(:user) { create(:user) } + let(:expected_years) { (year..Time.current.year).to_a.reverse } + + before do + create(:stat, year: 2021, user: user) + create(:stat, year: 2020, user: user) + end + + it 'returns years' do + expect(subject).to eq(expected_years) + end + end + end + + describe '#distance_by_day' do + subject { stat.distance_by_day } + + let(:user) { create(:user) } + let(:stat) { create(:stat, year: year, month: 1, user: user) } + let(:expected_distance) do + # 31 day of January + (1..31).map { |day| [day, 0] } + end + + context 'when there are points' do + let!(:points) do + create(:point, latitude: 1, longitude: 1, timestamp: DateTime.new(year, 1, 1, 1)) + create(:point, latitude: 2, longitude: 2, timestamp: DateTime.new(year, 1, 1, 2)) + end + + before { expected_distance[0][1] = 157.23 } + + it 'returns distance by day' do + expect(subject).to eq(expected_distance) + end + end + + context 'when there are no points' do + it 'returns distance by day' do + expect(subject).to eq(expected_distance) + end + end + end + + describe '#timespan' do + subject { stat.send(:timespan) } + + let(:stat) { build(:stat, year: year, month: 1) } + let(:expected_timespan) { DateTime.new(year, 1).beginning_of_month..DateTime.new(year, 1).end_of_month } + + it 'returns timespan' do + expect(subject).to eq(expected_timespan) + end + end + + describe '#self.year_distance' do + subject { described_class.year_distance(year) } + + let(:user) { create(:user) } + let(:expected_distance) do + (1..12).map { |month| [Date::MONTHNAMES[month], 0] } + end + + context 'when there are stats' do + let!(:stats) do + create(:stat, year: year, month: 1, distance: 100, user: user) + create(:stat, year: year, month: 2, distance: 200, user: user) + end + + before do + expected_distance[0][1] = 100 + expected_distance[1][1] = 200 + end + + it 'returns year distance' do + expect(subject).to eq(expected_distance) + end + end + + context 'when there are no stats' do + it 'returns year distance' do + expect(subject).to eq(expected_distance) + end + end + end + end end diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index b12bcf29..9809ac0a 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -1,7 +1,68 @@ require 'rails_helper' RSpec.describe User, type: :model do - it { is_expected.to have_many(:imports).dependent(:destroy) } - it { is_expected.to have_many(:points).through(:imports) } - it { is_expected.to have_many(:stats) } + describe 'associations' do + it { is_expected.to have_many(:imports).dependent(:destroy) } + it { is_expected.to have_many(:points).through(:imports) } + it { is_expected.to have_many(:stats) } + end + + describe 'methods' do + let(:user) { create(:user) } + + xdescribe '#export_data' do + subject { user.export_data } + + let(:import) { create(:import, user: user) } + let(:point) { create(:point, import: import) } + + it 'returns json' do + expect(subject).to include(user.email) + expect(subject).to include('dawarich-export') + expect(subject).to include(point.attributes.except('raw_data', 'id', 'created_at', 'updated_at', 'country', 'city', 'import_id').to_json) + end + end + + describe '#total_km' do + subject { user.total_km } + + let!(:stat_1) { create(:stat, user: user, distance: 10) } + let!(:stat_2) { create(:stat, user: user, distance: 20) } + + it 'returns sum of distances' do + expect(subject).to eq(30) + end + end + + describe '#total_countries' do + subject { user.total_countries } + + let!(:stat) { create(:stat, user: user, toponyms: [{ 'country' => 'Country' }]) } + + it 'returns number of countries' do + expect(subject).to eq(1) + end + end + + describe '#total_cities' do + subject { user.total_cities } + + let!(:stat) { create(:stat, user: user, toponyms: [{ 'city' => 'City' }]) } + + it 'returns number of cities' do + expect(subject).to eq(1) + end + end + + describe '#total_reverse_geocoded' do + subject { user.total_reverse_geocoded } + + let(:import) { create(:import, user: user) } + let!(:point) { create(:point, country: 'Country', city: 'City', import: import) } + + it 'returns number of reverse geocoded points' do + expect(subject).to eq(1) + end + end + end end