mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 01:31:39 -05:00
commit
def0eba421
30 changed files with 540 additions and 72 deletions
|
|
@ -1 +1,4 @@
|
|||
require: rubocop-rails
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
|
|
|||
7
Gemfile
7
Gemfile
|
|
@ -15,10 +15,11 @@ gem 'stimulus-rails'
|
|||
gem 'tailwindcss-rails'
|
||||
gem 'turbo-rails'
|
||||
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
|
||||
gem "importmap-rails"
|
||||
gem "chartkick"
|
||||
gem 'importmap-rails'
|
||||
gem 'chartkick'
|
||||
gem 'geocoder'
|
||||
gem 'sidekiq'
|
||||
gem 'sidekiq-cron'
|
||||
|
||||
|
||||
group :development, :test do
|
||||
|
|
@ -42,4 +43,4 @@ group :development do
|
|||
end
|
||||
|
||||
# Use Redis for Action Cable
|
||||
gem "redis"
|
||||
gem 'redis'
|
||||
|
|
|
|||
11
Gemfile.lock
11
Gemfile.lock
|
|
@ -106,6 +106,8 @@ GEM
|
|||
railties (>= 6.1)
|
||||
drb (2.2.1)
|
||||
erubi (1.12.0)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
factory_bot (6.4.6)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.4.3)
|
||||
|
|
@ -113,6 +115,9 @@ GEM
|
|||
railties (>= 5.0.0)
|
||||
ffaker (2.23.0)
|
||||
foreman (0.87.2)
|
||||
fugit (1.10.1)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
geocoder (1.8.2)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
|
|
@ -178,6 +183,7 @@ GEM
|
|||
nio4r (~> 2.0)
|
||||
pundit (2.3.1)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.7.3)
|
||||
rack (3.0.10)
|
||||
rack-session (2.0.0)
|
||||
|
|
@ -274,6 +280,10 @@ GEM
|
|||
connection_pool (>= 2.3.0)
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.19.0)
|
||||
sidekiq-cron (1.12.0)
|
||||
fugit (~> 1.8)
|
||||
globalid (>= 1.0.1)
|
||||
sidekiq (>= 6)
|
||||
simplecov (0.22.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
|
|
@ -343,6 +353,7 @@ DEPENDENCIES
|
|||
rubocop-rails
|
||||
shoulda-matchers
|
||||
sidekiq
|
||||
sidekiq-cron
|
||||
simplecov
|
||||
sprockets-rails
|
||||
stimulus-rails
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
web: bin/rails server -p 3000 -b 0.0.0.0
|
||||
css: bin/rails tailwindcss:watch
|
||||
worker: bundle exec sidekiq
|
||||
worker: bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
|
|
|||
|
|
@ -2,15 +2,9 @@ class Api::V1::PointsController < ApplicationController
|
|||
skip_forgery_protection
|
||||
|
||||
def create
|
||||
parsed_params = OwnTracks::Params.new(point_params).call
|
||||
PointCreatingJob.perform_later(point_params)
|
||||
|
||||
@point = Point.create(parsed_params)
|
||||
|
||||
if @point.valid?
|
||||
render json: @point, status: :ok
|
||||
else
|
||||
render json: @point.errors, status: :unprocessable_entity
|
||||
end
|
||||
render json: {}, status: :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class PointsController < ApplicationController
|
|||
@distance = distance
|
||||
@start_at = Time.zone.at(start_at)
|
||||
@end_at = Time.zone.at(end_at)
|
||||
@years = (@start_at.year..@end_at.year).to_a
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -18,13 +19,13 @@ class PointsController < ApplicationController
|
|||
def start_at
|
||||
return 1.month.ago.beginning_of_day.to_i if params[:start_at].nil?
|
||||
|
||||
params[:start_at].to_datetime.to_i
|
||||
Time.parse(params[:start_at]).to_i
|
||||
end
|
||||
|
||||
def end_at
|
||||
return Time.zone.today.end_of_day.to_i if params[:end_at].nil?
|
||||
|
||||
params[:end_at].to_datetime.to_i
|
||||
Time.parse(params[:end_at]).to_i
|
||||
end
|
||||
|
||||
def distance
|
||||
|
|
|
|||
|
|
@ -17,10 +17,18 @@ module ApplicationHelper
|
|||
end
|
||||
|
||||
def year_timespan(year)
|
||||
start_at = DateTime.new(year).beginning_of_year.to_time.strftime('%Y-%m-%dT%H:%M')
|
||||
end_at = DateTime.new(year).end_of_year.to_time.strftime('%Y-%m-%dT%H:%M')
|
||||
start_at = Time.utc(year).in_time_zone('Europe/Berlin').beginning_of_year.strftime('%Y-%m-%dT%H:%M')
|
||||
end_at = Time.utc(year).in_time_zone('Europe/Berlin').end_of_year.strftime('%Y-%m-%dT%H:%M')
|
||||
|
||||
{ start_at: start_at, end_at: end_at }
|
||||
{ start_at:, end_at: }
|
||||
end
|
||||
|
||||
def timespan(month, year)
|
||||
month = DateTime.new(year, month).in_time_zone(Time.zone)
|
||||
start_at = month.beginning_of_month.to_time.strftime('%Y-%m-%dT%H:%M')
|
||||
end_at = month.end_of_month.to_time.strftime('%Y-%m-%dT%H:%M')
|
||||
|
||||
{ start_at:, end_at: }
|
||||
end
|
||||
|
||||
def header_colors
|
||||
|
|
@ -38,4 +46,14 @@ module ApplicationHelper
|
|||
def year_distance_stat_in_km(year)
|
||||
Stat.year_distance(year).sum { _1[1] }
|
||||
end
|
||||
|
||||
def is_past?(year, month)
|
||||
DateTime.new(year, month).past?
|
||||
end
|
||||
|
||||
def points_exist?(year, month)
|
||||
Point.where(
|
||||
timestamp: DateTime.new(year, month).beginning_of_month..DateTime.new(year, month).end_of_month
|
||||
).exists?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
9
app/jobs/point_creating_job.rb
Normal file
9
app/jobs/point_creating_job.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class PointCreatingJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(point_params)
|
||||
parsed_params = OwnTracks::Params.new(point_params).call
|
||||
|
||||
point = Point.create(parsed_params)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StatCreatingJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(user_id)
|
||||
CreateStats.new(user_id).call
|
||||
def perform(user_ids = nil)
|
||||
CreateStats.new(user_ids).call
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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,6 +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 = 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
|
||||
|
|
|
|||
|
|
@ -37,8 +37,10 @@ class CountriesAndCities
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
def filter_cities(mapped_with_cities)
|
||||
# In future, we would want to remove cities where user spent less than
|
||||
# 1 hour per day
|
||||
|
||||
# Remove cities with less than MINIMUM_POINTS_IN_CITY
|
||||
mapped_with_cities.transform_values do |cities|
|
||||
cities.reject { |_, data| data[:points] < MINIMUM_POINTS_IN_CITY }
|
||||
|
|
@ -48,8 +50,8 @@ class CountriesAndCities
|
|||
def normalize_result(hash)
|
||||
hash.map do |country, cities|
|
||||
{
|
||||
country: country,
|
||||
cities: cities.map { |city, data| { city: city, points: data[:points], timestamp: data[:timestamp] } }
|
||||
country:,
|
||||
cities: cities.map { |city, data| { city:, points: data[:points], timestamp: data[:timestamp] } }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,32 +1,34 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateStats
|
||||
attr_reader :years, :months, :user
|
||||
attr_reader :years, :months, :users
|
||||
|
||||
def initialize(user_id)
|
||||
@user = User.find(user_id)
|
||||
def initialize(user_ids)
|
||||
@users = User.where(id: user_ids)
|
||||
@years = (1970..Time.current.year).to_a
|
||||
@months = (1..12).to_a
|
||||
end
|
||||
|
||||
def call
|
||||
years.flat_map do |year|
|
||||
months.map do |month|
|
||||
beginning_of_month_timestamp = DateTime.new(year, month).beginning_of_month.to_i
|
||||
end_of_month_timestamp = DateTime.new(year, month).end_of_month.to_i
|
||||
users.each do |user|
|
||||
years.each do |year|
|
||||
months.each do |month|
|
||||
beginning_of_month_timestamp = DateTime.new(year, month).beginning_of_month.to_i
|
||||
end_of_month_timestamp = DateTime.new(year, month).end_of_month.to_i
|
||||
|
||||
points = points(beginning_of_month_timestamp, end_of_month_timestamp)
|
||||
next if points.empty?
|
||||
points = points(beginning_of_month_timestamp, end_of_month_timestamp)
|
||||
next if points.empty?
|
||||
|
||||
stat = Stat.find_or_initialize_by(year: year, month: month, user: user)
|
||||
stat.distance = distance(points)
|
||||
stat.toponyms = toponyms(points)
|
||||
stat.daily_distance = stat.distance_by_day
|
||||
stat.save
|
||||
stat = Stat.find_or_initialize_by(year: year, month: month, user: user)
|
||||
stat.distance = distance(points)
|
||||
stat.toponyms = toponyms(points)
|
||||
stat.daily_distance = stat.distance_by_day
|
||||
stat.save
|
||||
|
||||
stat
|
||||
stat
|
||||
end
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.7.2/dist/full.css" rel="stylesheet" type="text/css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.9.0/dist/full.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
|
||||
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class='w-3/4 mt-10'>
|
||||
<div class='w-4/5 mt-10'>
|
||||
<div class="flex flex-col space-y-4 mb-4 w-full">
|
||||
<%= form_with url: points_path, method: :get do |f| %>
|
||||
<div class="flex flex-col md:flex-row md:space-x-4 md:items-end">
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
<div class='w-1/4 mt-10'>
|
||||
<div class='w-1/5 mt-10'>
|
||||
<%= render 'shared/right_sidebar' %>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,36 @@
|
|||
<%= "#{@distance} km" if @distance %>
|
||||
|
||||
<div id='years-nav'>
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn m-1">Select year</div>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<% Stat.years.each do |year| %>
|
||||
<li><%= link_to year, points_url(year_timespan(year).merge(year: year)) %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<% @years.each do |year| %>
|
||||
<h3 class='text-xl'>
|
||||
<%= year %>
|
||||
</h3>
|
||||
|
||||
<div class='grid grid-cols-3 gap-3'>
|
||||
<% (1..12).to_a.each_slice(3) do |months| %>
|
||||
<% months.each do |month_number| %>
|
||||
<% if is_past?(year, month_number) && points_exist?(year, month_number) %>
|
||||
<%= link_to Date::ABBR_MONTHNAMES[month_number], points_url(timespan(month_number, year)), class: 'btn btn-default' %>
|
||||
<% else %>
|
||||
<div class='btn btn-disabled'><%= Date::ABBR_MONTHNAMES[month_number] %></div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if REVERSE_GEOCODING_ENABLED && @countries_and_cities&.any? %>
|
||||
<hr class='my-5'>
|
||||
<% @countries_and_cities.each do |country| %>
|
||||
<h2 class="text-lg font-semibold mt-5">
|
||||
<%= country[:country] %> (<%= country[:cities].count %> cities)
|
||||
|
|
|
|||
|
|
@ -48,10 +48,16 @@
|
|||
<%= link_to year, "/stats/#{year}", class: 'underline hover:no-underline' %>
|
||||
<%= link_to '[Map]', points_url(year_timespan(year)), class: 'underline hover:no-underline' %>
|
||||
</h2>
|
||||
<p><%= number_with_delimiter year_distance_stat_in_km(year) %>km</p>
|
||||
<p>
|
||||
<% cache [current_user, 'year_distance_stat_in_km', year], skip_digest: true do %>
|
||||
<%= number_with_delimiter year_distance_stat_in_km(year) %>km
|
||||
<% end %>
|
||||
</p>
|
||||
<% if REVERSE_GEOCODING_ENABLED %>
|
||||
<div class="card-actions justify-end">
|
||||
<%= countries_and_cities_stat(year) %>
|
||||
<% cache [current_user, 'countries_and_cities_stat', year], skip_digest: true do %>
|
||||
<%= countries_and_cities_stat(year) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= column_chart(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
MINIMUM_POINTS_IN_CITY = ENV.fetch('MINIMUM_POINTS_IN_CITY', 5).to_i
|
||||
MAP_CENTER = ENV.fetch('MAP_CENTER', '[55.7522, 37.6156]')
|
||||
REVERSE_GEOCODING_ENABLED = ENV.fetch('REVERSE_GEOCODING_ENABLED', 'true') == 'true'
|
||||
|
|
|
|||
6
config/schedule.yml
Normal file
6
config/schedule.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# config/schedule.yml
|
||||
|
||||
stat_creating_job:
|
||||
cron: "0 */6 * * *"
|
||||
class: "StatCreatingJob"
|
||||
queue: default
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
FactoryBot.define do
|
||||
factory :import do
|
||||
user_id { "" }
|
||||
user
|
||||
name { 'APRIL_2013.json' }
|
||||
source { 1 }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ FactoryBot.define do
|
|||
raw_data { "" }
|
||||
tracker_id { "MyString" }
|
||||
import_id { "" }
|
||||
city { "MyString" }
|
||||
country { "MyString" }
|
||||
city { nil }
|
||||
country { nil }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,22 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ImportJob, type: :job do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
describe '#perform' do
|
||||
subject(:perform) { described_class.new.perform(user.id, import.id) }
|
||||
|
||||
let(:file_path) { 'spec/fixtures/owntracks_export.json' }
|
||||
let(:file) { fixture_file_upload(file_path) }
|
||||
let(:user) { create(:user) }
|
||||
let(:import) { create(:import, user: user, file: file, name: File.basename(file.path)) }
|
||||
|
||||
it 'creates points' do
|
||||
expect { perform }.to change { Point.count }.by(8)
|
||||
end
|
||||
|
||||
it 'calls StatCreatingJob' do
|
||||
expect(StatCreatingJob).to receive(:perform_later).with(user.id)
|
||||
|
||||
perform
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
15
spec/jobs/point_creating_job_spec.rb
Normal file
15
spec/jobs/point_creating_job_spec.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PointCreatingJob, type: :job do
|
||||
describe '#perform' do
|
||||
subject(:perform) { described_class.new.perform(point_params) }
|
||||
|
||||
let(:point_params) do
|
||||
{ lat: 1.0, lon: 1.0, tid: 'test', tst: Time.now.to_i, topic: 'iPhone 12 pro' }
|
||||
end
|
||||
|
||||
it 'creates a point' do
|
||||
expect { perform }.to change { Point.count }.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
61
spec/jobs/reverse_geocoding_job_spec.rb
Normal file
61
spec/jobs/reverse_geocoding_job_spec.rb
Normal file
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -12,18 +12,11 @@ RSpec.describe "Api::V1::Points", type: :request do
|
|||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid params' do
|
||||
let(:params) do
|
||||
{ lat: 1.0, lon: 1.0, tid: 'test', tst: Time.now.to_i }
|
||||
end
|
||||
|
||||
it "returns http unprocessable_entity" do
|
||||
post api_v1_points_path, params: params
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to eq("{\"topic\":[\"can't be blank\"]}")
|
||||
it 'enqueues a job' do
|
||||
expect {
|
||||
post api_v1_points_path, params: params
|
||||
}.to have_enqueued_job(PointCreatingJob)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
47
spec/services/countries_and_cities_spec.rb
Normal file
47
spec/services/countries_and_cities_spec.rb
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe CountriesAndCities do
|
||||
describe '#call' do
|
||||
subject(:countries_and_cities) { described_class.new(points).call }
|
||||
|
||||
let(:points) do
|
||||
[
|
||||
create(:point, latitude: 0, longitude: 0, city: 'City', country: 'Country'),
|
||||
create(:point, latitude: 1, longitude: 1, city: 'City', country: 'Country'),
|
||||
create(:point, latitude: 2, longitude: 2, city: 'City', country: 'Country'),
|
||||
create(:point, latitude: 2, longitude: 2, city: 'Another city', country: 'Some Country'),
|
||||
create(:point, latitude: 2, longitude: 6, city: 'Another city', country: 'Some Country')
|
||||
]
|
||||
end
|
||||
|
||||
context 'when MINIMUM_POINTS_IN_CITY is 1' do
|
||||
before do
|
||||
stub_const('CountriesAndCities::MINIMUM_POINTS_IN_CITY', 1)
|
||||
end
|
||||
|
||||
it 'returns countries and cities' do
|
||||
expect(countries_and_cities).to eq(
|
||||
[
|
||||
{ cities: [{city: "City", points: 3, timestamp: 1}], country: "Country" },
|
||||
{ cities: [{city: "Another city", points: 2, timestamp: 1}], country: "Some Country" }
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when MINIMUM_POINTS_IN_CITY is 3' do
|
||||
before do
|
||||
stub_const('CountriesAndCities::MINIMUM_POINTS_IN_CITY', 3)
|
||||
end
|
||||
|
||||
it 'returns countries and cities' do
|
||||
expect(countries_and_cities).to eq(
|
||||
[
|
||||
{ cities: [{city: "City", points: 3, timestamp: 1}], country: "Country" },
|
||||
{ cities: [], country: "Some Country" }
|
||||
]
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
34
spec/services/create_stats_spec.rb
Normal file
34
spec/services/create_stats_spec.rb
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe CreateStats do
|
||||
describe '#call' do
|
||||
subject(:create_stats) { described_class.new(user_ids).call }
|
||||
|
||||
let(:user_ids) { [user.id] }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'when there are no points' do
|
||||
it 'does not create stats' do
|
||||
expect { create_stats }.not_to change { Stat.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are points' do
|
||||
let!(:import) { create(:import, user: user) }
|
||||
let!(:point_1) { create(:point, import: import, latitude: 0, longitude: 0) }
|
||||
let!(:point_2) { create(:point, import: import, latitude: 1, longitude: 2) }
|
||||
let!(:point_3) { create(:point, import: import, latitude: 3, longitude: 4) }
|
||||
|
||||
|
||||
it 'creates stats' do
|
||||
expect { create_stats }.to change { Stat.count }.by(1)
|
||||
end
|
||||
|
||||
it 'calculates distance' do
|
||||
create_stats
|
||||
|
||||
expect(Stat.last.distance).to eq(563)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2,7 +2,7 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe OwnTracks::ExportParser do
|
||||
describe '#call' do
|
||||
subject(:parser) { described_class.new(import.id).call }
|
||||
subject(:parser) { described_class.new(import).call }
|
||||
|
||||
let(:file_path) { 'spec/fixtures/owntracks_export.json' }
|
||||
let(:file) { fixture_file_upload(file_path) }
|
||||
|
|
|
|||
Loading…
Reference in a new issue