+
Visits
+
+ Order by:
+ <%= link_to 'Newest', visits_path(order_by: :desc), class: 'btn btn-xs btn-primary mx-1' %>
+ <%= link_to 'Oldest', visits_path(order_by: :asc), class: 'btn btn-xs btn-primary mx-1' %>
+
<% if @visits.empty? %>
diff --git a/config/routes.rb b/config/routes.rb
index 3a26b3a6..2e13c740 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -56,7 +56,7 @@ Rails.application.routes.draw do
namespace :api do
namespace :v1 do
resources :areas, only: %i[index create update destroy]
- resources :points, only: %i[destroy]
+ resources :points, only: %i[index destroy]
namespace :overland do
resources :batches, only: :create
diff --git a/db/data/20240730130922_add_route_opacity_to_settings.rb b/db/data/20240730130922_add_route_opacity_to_settings.rb
new file mode 100644
index 00000000..2742b146
--- /dev/null
+++ b/db/data/20240730130922_add_route_opacity_to_settings.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class AddRouteOpacityToSettings < ActiveRecord::Migration[7.1]
+ def up
+ User.find_each do |user|
+ user.settings = user.settings.merge(route_opacity: 20)
+ user.save!
+ end
+ end
+
+ def down
+ raise ActiveRecord::IrreversibleMigration
+ end
+end
diff --git a/db/data_schema.rb b/db/data_schema.rb
index c512cb50..8dfd037d 100644
--- a/db/data_schema.rb
+++ b/db/data_schema.rb
@@ -1 +1 @@
-DataMigrate::Data.define(version: 20240625201842)
+DataMigrate::Data.define(version: 20240730130922)
diff --git a/lib/tasks/import.rake b/lib/tasks/import.rake
index 76b44f23..72f7d1ba 100644
--- a/lib/tasks/import.rake
+++ b/lib/tasks/import.rake
@@ -6,22 +6,6 @@ namespace :import do
desc 'Accepts a file path and user email and imports the data into the database'
task :big_file, %i[file_path user_email] => :environment do |_, args|
- user = User.find_by(email: args[:user_email])
-
- raise 'User not found' unless user
-
- import = user.imports.create(name: args[:file_path], source: :google_records)
- import_id = import.id
-
- pp "Importing #{args[:file_path]} for #{user.email}, file size is #{File.size(args[:file_path])}... This might take a while, have patience!"
-
- content = File.read(args[:file_path]); nil
- data = Oj.load(content); nil
-
- data['locations'].each do |json|
- ImportGoogleTakeoutJob.perform_later(import_id, json.to_json)
- end
-
- pp "Imported #{args[:file_path]} for #{user.email} successfully! Wait for the processing to finish. You can check the status of the import in the Sidekiq UI (http://
/sidekiq)."
+ Tasks::Imports::GoogleRecords.new(args[:file_path], args[:user_email]).call
end
end
diff --git a/spec/factories/stats.rb b/spec/factories/stats.rb
index fdf1ed19..bcfd1324 100644
--- a/spec/factories/stats.rb
+++ b/spec/factories/stats.rb
@@ -1,8 +1,19 @@
+# frozen_string_literal: true
+
FactoryBot.define do
factory :stat do
year { 1 }
month { 1 }
distance { 1 }
- toponyms { "" }
+ toponyms do
+ [
+ {
+ 'cities' => [
+ { 'city' => 'Moscow', 'points' => 7, 'timestamp' => 1_554_317_696, 'stayed_for' => 1831 }
+ ],
+ 'country' => 'Russia'
+ }, { 'cities' => [], 'country' => nil }
+ ]
+ end
end
end
diff --git a/spec/jobs/area_visits_calculating_job_spec.rb b/spec/jobs/area_visits_calculating_job_spec.rb
index 9c4e75e3..46185a76 100644
--- a/spec/jobs/area_visits_calculating_job_spec.rb
+++ b/spec/jobs/area_visits_calculating_job_spec.rb
@@ -1,5 +1,16 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe AreaVisitsCalculatingJob, type: :job do
- pending "add some examples to (or delete) #{__FILE__}"
+ describe '#perform' do
+ let(:user) { create(:user) }
+ let(:area) { create(:area, user:) }
+
+ it 'calls the AreaVisitsCalculationService' do
+ expect(Areas::Visits::Create).to receive(:new).with(user, [area]).and_call_original
+
+ described_class.new.perform(user.id)
+ end
+ end
end
diff --git a/spec/jobs/area_visits_calculation_scheduling_job_spec.rb b/spec/jobs/area_visits_calculation_scheduling_job_spec.rb
index 71607985..0d375e67 100644
--- a/spec/jobs/area_visits_calculation_scheduling_job_spec.rb
+++ b/spec/jobs/area_visits_calculation_scheduling_job_spec.rb
@@ -1,5 +1,16 @@
+# frozen_string_literal: true
+
require 'rails_helper'
RSpec.describe AreaVisitsCalculationSchedulingJob, type: :job do
- pending "add some examples to (or delete) #{__FILE__}"
+ describe '#perform' do
+ let(:area) { create(:area) }
+ let(:user) { create(:user) }
+
+ it 'calls the AreaVisitsCalculationService' do
+ expect(AreaVisitsCalculatingJob).to receive(:perform_later).with(user.id).and_call_original
+
+ described_class.new.perform
+ end
+ end
end
diff --git a/spec/models/point_spec.rb b/spec/models/point_spec.rb
index 3129f600..8e162c79 100644
--- a/spec/models/point_spec.rb
+++ b/spec/models/point_spec.rb
@@ -33,4 +33,22 @@ RSpec.describe Point, type: :model do
end
end
end
+
+ describe 'methods' do
+ describe '#recorded_at' do
+ let(:point) { create(:point, timestamp: 1_554_317_696) }
+
+ it 'returns recorded at time' do
+ expect(point.recorded_at).to eq(Time.zone.at(1_554_317_696))
+ end
+ end
+
+ describe '#async_reverse_geocode' do
+ let(:point) { build(:point) }
+
+ it 'enqueues ReverseGeocodeJob' do
+ expect { point.async_reverse_geocode }.to have_enqueued_job(ReverseGeocodingJob)
+ end
+ end
+ end
end
diff --git a/spec/requests/stats_spec.rb b/spec/requests/stats_spec.rb
index 0b57f02d..1efccab6 100644
--- a/spec/requests/stats_spec.rb
+++ b/spec/requests/stats_spec.rb
@@ -8,10 +8,55 @@ RSpec.describe '/stats', type: :request do
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
end
- describe 'GET /index' do
- it 'renders a successful response' do
- get stats_url
- expect(response.status).to eq(302)
+ context 'when user is not signed in' do
+ describe 'GET /index' do
+ it 'redirects to the sign in page' do
+ get stats_url
+
+ expect(response.status).to eq(302)
+ end
+ end
+
+ describe 'GET /show' do
+ it 'redirects to the sign in page' do
+ get stats_url(2024)
+
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+
+ context 'when user is signed in' do
+ before do
+ sign_in user
+ end
+
+ let(:user) { create(:user) }
+
+ describe 'GET /index' do
+ it 'renders a successful response' do
+ get stats_url
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ describe 'GET /show' do
+ let(:stat) { create(:stat, user:, year: 2024) }
+
+ it 'renders a successful response' do
+ get stats_url(stat.year)
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ describe 'POST /update' do
+ let(:stat) { create(:stat, user:, year: 2024) }
+
+ it 'enqueues StatCreatingJob' do
+ expect { post stats_url(stat.year) }.to have_enqueued_job(StatCreatingJob)
+ end
end
end
end
diff --git a/spec/services/tasks/imports/google_records_spec.rb b/spec/services/tasks/imports/google_records_spec.rb
new file mode 100644
index 00000000..f160da0f
--- /dev/null
+++ b/spec/services/tasks/imports/google_records_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe Tasks::Imports::GoogleRecords do
+ describe '#call' do
+ let(:user) { create(:user) }
+ let(:file_path) { Rails.root.join('spec/fixtures/files/google/records.json') }
+
+ it 'schedules the ImportGoogleTakeoutJob' do
+ expect(ImportGoogleTakeoutJob).to receive(:perform_later).exactly(3).times
+
+ described_class.new(file_path, user.email).call
+ end
+ end
+end
diff --git a/spec/swagger/api/v1/areas_controller_spec.rb b/spec/swagger/api/v1/areas_controller_spec.rb
index 609e6d58..0bd50a31 100644
--- a/spec/swagger/api/v1/areas_controller_spec.rb
+++ b/spec/swagger/api/v1/areas_controller_spec.rb
@@ -25,13 +25,13 @@ describe 'Areas API', type: :request do
}
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
response '201', 'area created' do
- let(:area) { { name: 'Home', latitude: 40.7128, longitude: -74.0060, radius: 100 } }
+ let(:area) { { name: 'Home', latitude: 40.7128, longitude: -74.0060, radius: 100 } }
let(:api_key) { create(:user).api_key }
run_test!
end
response '422', 'invalid request' do
- let(:area) { { name: 'Home', latitude: 40.7128, longitude: -74.0060 } }
+ let(:area) { { name: 'Home', latitude: 40.7128, longitude: -74.0060 } }
let(:api_key) { create(:user).api_key }
run_test!
@@ -56,12 +56,30 @@ describe 'Areas API', type: :request do
required: %w[id name latitude longitude radius]
}
- let(:user) { create(:user) }
- let(:areas) { create_list(:area, 3, user:) }
+ let(:user) { create(:user) }
+ let(:areas) { create_list(:area, 3, user:) }
let(:api_key) { user.api_key }
run_test!
end
end
end
+
+ path '/api/v1/areas/{id}' do
+ delete 'Deletes an area' do
+ tags 'Areas'
+ produces 'application/json'
+ parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
+ parameter name: :id, in: :path, type: :string, required: true, description: 'Area ID'
+
+ response '200', 'area deleted' do
+ let(:user) { create(:user) }
+ let(:area) { create(:area, user:) }
+ let(:api_key) { user.api_key }
+ let(:id) { area.id }
+
+ run_test!
+ end
+ end
+ end
end
diff --git a/spec/swagger/api/v1/points_controller_spec.rb b/spec/swagger/api/v1/points_controller_spec.rb
new file mode 100644
index 00000000..148d5e94
--- /dev/null
+++ b/spec/swagger/api/v1/points_controller_spec.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'swagger_helper'
+
+describe 'Points API', type: :request do
+ path '/api/v1/points' do
+ get 'Retrieves all points' do
+ tags 'Points'
+ produces 'application/json'
+ parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
+ parameter name: :start_at, in: :query, type: :string,
+ description: 'Start date (i.e. 2024-02-03T13:00:03Z or 2024-02-03)'
+ parameter name: :end_at, in: :query, type: :string,
+ description: 'End date (i.e. 2024-02-03T13:00:03Z or 2024-02-03)'
+ response '200', 'points found' do
+ schema type: :array,
+ items: {
+ type: :object,
+ properties: {
+ id: { type: :integer },
+ battery_status: { type: :number },
+ ping: { type: :number },
+ battery: { type: :number },
+ tracker_id: { type: :string },
+ topic: { type: :string },
+ altitude: { type: :number },
+ longitude: { type: :number },
+ velocity: { type: :number },
+ trigger: { type: :string },
+ bssid: { type: :string },
+ ssid: { type: :string },
+ connection: { type: :string },
+ vertical_accuracy: { type: :number },
+ accuracy: { type: :number },
+ timestamp: { type: :number },
+ latitude: { type: :number },
+ mode: { type: :number },
+ inrids: { type: :array },
+ in_regions: { type: :array },
+ raw_data: { type: :string },
+ import_id: { type: :string },
+ city: { type: :string },
+ country: { type: :string },
+ created_at: { type: :string },
+ updated_at: { type: :string },
+ user_id: { type: :integer },
+ geodata: { type: :string },
+ visit_id: { type: :string }
+ }
+ }
+
+ let(:user) { create(:user) }
+ let(:areas) { create_list(:area, 3, user:) }
+ let(:api_key) { user.api_key }
+ let(:start_at) { Time.zone.now - 1.day }
+ let(:end_at) { Time.zone.now }
+ let(:points) { create_list(:point, 10, user:, timestamp: 2.hours.ago) }
+
+ run_test!
+ end
+ end
+ end
+
+ path '/api/v1/points/{id}' do
+ delete 'Deletes a point' do
+ tags 'Points'
+ produces 'application/json'
+ parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
+ parameter name: :id, in: :path, type: :string, required: true, description: 'Point ID'
+
+ response '200', 'point deleted' do
+ let(:user) { create(:user) }
+ let(:point) { create(:point, user:) }
+ let(:api_key) { user.api_key }
+ let(:id) { point.id }
+
+ run_test!
+ end
+ end
+ end
+end
diff --git a/spec/tasks/import_spec.rb b/spec/tasks/import_spec.rb
new file mode 100644
index 00000000..4cd785db
--- /dev/null
+++ b/spec/tasks/import_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe 'import.rake' do
+ let(:file_path) { Rails.root.join('spec/fixtures/files/google/records.json') }
+ let(:user) { create(:user) }
+
+ it 'calls importing class' do
+ expect(Tasks::Imports::GoogleRecords).to receive(:new).with(file_path, user.email).and_call_original.once
+
+ Rake::Task['import:big_file'].invoke(file_path, user.email)
+ end
+end
diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml
index 080d0828..c2b26e95 100644
--- a/swagger/v1/swagger.yaml
+++ b/swagger/v1/swagger.yaml
@@ -85,6 +85,27 @@ paths:
- latitude
- longitude
- radius
+ "/api/v1/areas/{id}":
+ delete:
+ summary: Deletes an area
+ tags:
+ - Areas
+ parameters:
+ - name: api_key
+ in: query
+ required: true
+ description: API Key
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ description: Area ID
+ schema:
+ type: string
+ responses:
+ '200':
+ description: area deleted
"/api/v1/overland/batches":
post:
summary: Creates a batch of points
@@ -283,6 +304,117 @@ paths:
isorcv: '2024-02-03T13:00:03Z'
isotst: '2024-02-03T13:00:03Z'
disptst: '2024-02-03 13:00:03'
+ "/api/v1/points":
+ get:
+ summary: Retrieves all points
+ tags:
+ - Points
+ parameters:
+ - name: api_key
+ in: query
+ required: true
+ description: API Key
+ schema:
+ type: string
+ - name: start_at
+ in: query
+ description: Start date (i.e. 2024-02-03T13:00:03Z or 2024-02-03)
+ schema:
+ type: string
+ - name: end_at
+ in: query
+ description: End date (i.e. 2024-02-03T13:00:03Z or 2024-02-03)
+ schema:
+ type: string
+ responses:
+ '200':
+ description: points found
+ content:
+ application/json:
+ schema:
+ type: array
+ items:
+ type: object
+ properties:
+ id:
+ type: integer
+ battery_status:
+ type: number
+ ping:
+ type: number
+ battery:
+ type: number
+ tracker_id:
+ type: string
+ topic:
+ type: string
+ altitude:
+ type: number
+ longitude:
+ type: number
+ velocity:
+ type: number
+ trigger:
+ type: string
+ bssid:
+ type: string
+ ssid:
+ type: string
+ connection:
+ type: string
+ vertical_accuracy:
+ type: number
+ accuracy:
+ type: number
+ timestamp:
+ type: number
+ latitude:
+ type: number
+ mode:
+ type: number
+ inrids:
+ type: array
+ in_regions:
+ type: array
+ raw_data:
+ type: string
+ import_id:
+ type: string
+ city:
+ type: string
+ country:
+ type: string
+ created_at:
+ type: string
+ updated_at:
+ type: string
+ user_id:
+ type: integer
+ geodata:
+ type: string
+ visit_id:
+ type: string
+ "/api/v1/points/{id}":
+ delete:
+ summary: Deletes a point
+ tags:
+ - Points
+ parameters:
+ - name: api_key
+ in: query
+ required: true
+ description: API Key
+ schema:
+ type: string
+ - name: id
+ in: path
+ required: true
+ description: Point ID
+ schema:
+ type: string
+ responses:
+ '200':
+ description: point deleted
servers:
- url: http://{defaultHost}
variables: