diff --git a/Gemfile b/Gemfile index eb432aed..2b3f7406 100644 --- a/Gemfile +++ b/Gemfile @@ -6,6 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" } ruby '3.2.3' gem 'bootsnap', require: false gem 'chartkick' +gem 'data_migrate' gem 'devise' gem 'geocoder' gem 'importmap-rails' diff --git a/Gemfile.lock b/Gemfile.lock index b2c9bfb7..fc2ea24e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -96,6 +96,9 @@ GEM rexml crass (1.0.6) csv (3.3.0) + data_migrate (9.4.0) + activerecord (>= 6.1) + railties (>= 6.1) date (3.3.4) debug (1.9.2) irb (~> 1.10) @@ -391,6 +394,7 @@ PLATFORMS DEPENDENCIES bootsnap chartkick + data_migrate debug devise dotenv-rails diff --git a/app/controllers/api/v1/overland/batches_controller.rb b/app/controllers/api/v1/overland/batches_controller.rb index 014b43f5..3aa3cd39 100644 --- a/app/controllers/api/v1/overland/batches_controller.rb +++ b/app/controllers/api/v1/overland/batches_controller.rb @@ -5,7 +5,7 @@ class Api::V1::Overland::BatchesController < ApplicationController before_action :authenticate_api_key def create - Overland::BatchCreatingJob.perform_later(batch_params) + Overland::BatchCreatingJob.perform_later(batch_params, current_api_user.id) render json: { result: 'ok' }, status: :created end diff --git a/app/controllers/api/v1/owntracks/points_controller.rb b/app/controllers/api/v1/owntracks/points_controller.rb index e610a07a..c28d6df7 100644 --- a/app/controllers/api/v1/owntracks/points_controller.rb +++ b/app/controllers/api/v1/owntracks/points_controller.rb @@ -5,7 +5,7 @@ class Api::V1::PointsController < ApplicationController before_action :authenticate_api_key def create - Owntracks::PointCreatingJob.perform_later(point_params) + Owntracks::PointCreatingJob.perform_later(point_params, current_user.id) render json: {}, status: :ok end diff --git a/app/controllers/export_controller.rb b/app/controllers/export_controller.rb index cbd3e712..833d88ef 100644 --- a/app/controllers/export_controller.rb +++ b/app/controllers/export_controller.rb @@ -24,7 +24,7 @@ class ExportController < ApplicationController end def start_at - first_point_timestamp = Point.order(timestamp: :asc)&.first&.timestamp + first_point_timestamp = current_user.tracked_points.order(timestamp: :asc)&.first&.timestamp @start_at ||= if params[:start_at].nil? && first_point_timestamp.present? @@ -37,7 +37,7 @@ class ExportController < ApplicationController end def end_at - last_point_timestamp = Point.order(timestamp: :desc)&.last&.timestamp + last_point_timestamp = current_user.tracked_points.order(timestamp: :desc)&.last&.timestamp @end_at ||= if params[:end_at].nil? && last_point_timestamp.present? diff --git a/app/controllers/map_controller.rb b/app/controllers/map_controller.rb index 01cee763..794b0dbd 100644 --- a/app/controllers/map_controller.rb +++ b/app/controllers/map_controller.rb @@ -3,7 +3,7 @@ class MapController < ApplicationController before_action :authenticate_user! def index - @points = Point.without_raw_data.where('timestamp >= ? AND timestamp <= ?', start_at, end_at).order(timestamp: :asc) + @points = current_user.tracked_points.without_raw_data.where('timestamp >= ? AND timestamp <= ?', start_at, end_at).order(timestamp: :asc) @countries_and_cities = CountriesAndCities.new(@points).call @coordinates = diff --git a/app/controllers/points_controller.rb b/app/controllers/points_controller.rb index 2c08c11d..179daf7a 100644 --- a/app/controllers/points_controller.rb +++ b/app/controllers/points_controller.rb @@ -5,7 +5,8 @@ class PointsController < ApplicationController def index @points = - Point + current_user + .tracked_points .without_raw_data .where('timestamp >= ? AND timestamp <= ?', start_at, end_at) .order(timestamp: :asc) @@ -16,9 +17,9 @@ class PointsController < ApplicationController end def bulk_destroy - Point.where(id: params[:point_ids].compact).destroy_all + current_user.tracked_points.where(id: params[:point_ids].compact).destroy_all - redirect_to points_url, notice: "Points were successfully destroyed.", status: :see_other + redirect_to points_url, notice: 'Points were successfully destroyed.', status: :see_other end private diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 0b1f8c90..52256b5f 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -38,7 +38,7 @@ module ApplicationHelper end def countries_and_cities_stat(year) - data = Stat.year_cities_and_countries(year) + data = Stat.year_cities_and_countries(year, user) countries = data[:countries] cities = data[:cities] diff --git a/app/jobs/overland/batch_creating_job.rb b/app/jobs/overland/batch_creating_job.rb index 6172e4d5..a3d6da1d 100644 --- a/app/jobs/overland/batch_creating_job.rb +++ b/app/jobs/overland/batch_creating_job.rb @@ -3,11 +3,11 @@ class Overland::BatchCreatingJob < ApplicationJob queue_as :default - def perform(params) + def perform(params, user_id) data = Overland::Params.new(params).call data.each do |location| - Point.create!(location) + Point.create!(location.merge(user_id:)) end end end diff --git a/app/jobs/owntracks/point_creating_job.rb b/app/jobs/owntracks/point_creating_job.rb index 928d75a0..d4df3971 100644 --- a/app/jobs/owntracks/point_creating_job.rb +++ b/app/jobs/owntracks/point_creating_job.rb @@ -3,9 +3,10 @@ class Owntracks::PointCreatingJob < ApplicationJob queue_as :default - def perform(point_params) + # TODO: after deprecation of old endpoint, make user_id required + def perform(point_params, user_id = nil) parsed_params = OwnTracks::Params.new(point_params).call - Point.create(parsed_params) + Point.create!(parsed_params.merge(user_id:)) end end diff --git a/app/models/point.rb b/app/models/point.rb index c15984e5..df627fc9 100644 --- a/app/models/point.rb +++ b/app/models/point.rb @@ -4,6 +4,7 @@ class Point < ApplicationRecord # self.ignored_columns = %w[raw_data] belongs_to :import, optional: true + belongs_to :user, optional: true validates :latitude, :longitude, :timestamp, presence: true diff --git a/app/models/stat.rb b/app/models/stat.rb index 3caa33c5..da10fece 100644 --- a/app/models/stat.rb +++ b/app/models/stat.rb @@ -11,7 +11,7 @@ class Stat < ApplicationRecord end_of_day = day.end_of_day.to_i # We have to filter by user as well - points = Point.without_raw_data.where(timestamp: beginning_of_day..end_of_day) + points = user.tracked_points.without_raw_data.where(timestamp: beginning_of_day..end_of_day) data = { day: index, distance: 0 } @@ -40,8 +40,8 @@ class Stat < ApplicationRecord end end - def self.year_cities_and_countries(year) - points = Point.where(timestamp: DateTime.new(year).beginning_of_year..DateTime.new(year).end_of_year) + def self.year_cities_and_countries(year, user) + points = user.tracked_points.where(timestamp: DateTime.new(year).beginning_of_year..DateTime.new(year).end_of_year) data = CountriesAndCities.new(points).call diff --git a/app/models/user.rb b/app/models/user.rb index de70dd3e..2201ef14 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -9,6 +9,7 @@ class User < ApplicationRecord has_many :imports, dependent: :destroy has_many :points, through: :imports has_many :stats, dependent: :destroy + has_many :tracked_points, class_name: 'Point', dependent: :destroy after_create :create_api_key diff --git a/app/views/stats/index.html.erb b/app/views/stats/index.html.erb index 92951adb..33508af3 100644 --- a/app/views/stats/index.html.erb +++ b/app/views/stats/index.html.erb @@ -83,7 +83,7 @@ <% if REVERSE_GEOCODING_ENABLED %>
<% cache [current_user, 'countries_and_cities_stat', year], skip_digest: true do %> - <%= countries_and_cities_stat(year) %> + <%= countries_and_cities_stat(year, current_user) %> <% end %>
<% end %> diff --git a/db/data/20240525110530_bind_existing_points_to_first_user.rb b/db/data/20240525110530_bind_existing_points_to_first_user.rb new file mode 100644 index 00000000..21fab9a4 --- /dev/null +++ b/db/data/20240525110530_bind_existing_points_to_first_user.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class BindExistingPointsToFirstUser < ActiveRecord::Migration[7.1] + def up + user = User.first + + points = Point.where(user_id: nil) + + points.update_all(user_id: user.id) + + Rails.logger.info "Bound #{points.count} points to user #{user.email}" + end + + def down + raise ActiveRecord::IrreversibleMigration + end +end diff --git a/db/data_schema.rb b/db/data_schema.rb new file mode 100644 index 00000000..394d4c85 --- /dev/null +++ b/db/data_schema.rb @@ -0,0 +1 @@ +DataMigrate::Data.define(version: 20240525110530) diff --git a/db/migrate/20240525110244_add_user_id_to_points.rb b/db/migrate/20240525110244_add_user_id_to_points.rb new file mode 100644 index 00000000..9715621d --- /dev/null +++ b/db/migrate/20240525110244_add_user_id_to_points.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddUserIdToPoints < ActiveRecord::Migration[7.1] + def change + add_reference :points, :user, foreign_key: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 867f1b87..4130dfc1 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.1].define(version: 2024_05_18_095848) do +ActiveRecord::Schema[7.1].define(version: 2024_05_25_110244) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -82,6 +82,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_18_095848) do t.string "country" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.bigint "user_id" t.index ["altitude"], name: "index_points_on_altitude" t.index ["battery"], name: "index_points_on_battery" t.index ["battery_status"], name: "index_points_on_battery_status" @@ -92,6 +93,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_18_095848) do t.index ["latitude", "longitude"], name: "index_points_on_latitude_and_longitude" t.index ["timestamp"], name: "index_points_on_timestamp" t.index ["trigger"], name: "index_points_on_trigger" + t.index ["user_id"], name: "index_points_on_user_id" end create_table "stats", force: :cascade do |t| @@ -125,5 +127,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_18_095848) do add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id" add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id" + add_foreign_key "points", "users" add_foreign_key "stats", "users" end diff --git a/dev-docker-entrypoint.sh b/dev-docker-entrypoint.sh index f50f6906..a21b8738 100644 --- a/dev-docker-entrypoint.sh +++ b/dev-docker-entrypoint.sh @@ -28,5 +28,9 @@ bundle exec rails db:create echo "PostgreSQL is ready. Running database migrations..." bundle exec rails db:prepare +# Run data migrations +echo "Running DATA migrations..." +bundle exec rake data:migrate + # run passed commands bundle exec ${@} diff --git a/spec/jobs/overland/batch_creating_job_spec.rb b/spec/jobs/overland/batch_creating_job_spec.rb index 63f5e2c8..d1d842c8 100644 --- a/spec/jobs/overland/batch_creating_job_spec.rb +++ b/spec/jobs/overland/batch_creating_job_spec.rb @@ -2,14 +2,21 @@ require 'rails_helper' RSpec.describe Overland::BatchCreatingJob, type: :job do describe '#perform' do - subject(:perform) { described_class.new.perform(json) } + subject(:perform) { described_class.new.perform(json, user.id) } let(:file_path) { 'spec/fixtures/files/overland/geodata.json' } let(:file) { File.open(file_path) } let(:json) { JSON.parse(file.read) } + let(:user) { create(:user) } it 'creates a location' do expect { perform }.to change { Point.count }.by(1) end + + it 'creates a point with the correct user_id' do + perform + + expect(Point.last.user_id).to eq(user.id) + end end end diff --git a/spec/jobs/owntracks/point_creating_job_spec.rb b/spec/jobs/owntracks/point_creating_job_spec.rb index 625fbce6..7c15b5d3 100644 --- a/spec/jobs/owntracks/point_creating_job_spec.rb +++ b/spec/jobs/owntracks/point_creating_job_spec.rb @@ -2,14 +2,21 @@ require 'rails_helper' RSpec.describe Owntracks::PointCreatingJob, type: :job do describe '#perform' do - subject(:perform) { described_class.new.perform(point_params) } + subject(:perform) { described_class.new.perform(point_params, user.id) } let(:point_params) do { lat: 1.0, lon: 1.0, tid: 'test', tst: Time.now.to_i, topic: 'iPhone 12 pro' } end + let(:user) { create(:user) } it 'creates a point' do expect { perform }.to change { Point.count }.by(1) end + + it 'creates a point with the correct user_id' do + perform + + expect(Point.last.user_id).to eq(user.id) + end end end diff --git a/spec/models/point_spec.rb b/spec/models/point_spec.rb index 5ff7afdb..4548a9f6 100644 --- a/spec/models/point_spec.rb +++ b/spec/models/point_spec.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + require 'rails_helper' RSpec.describe Point, type: :model do describe 'associations' do it { is_expected.to belong_to(:import).optional } + it { is_expected.to belong_to(:user).optional } end describe 'validations' do diff --git a/spec/models/stat_spec.rb b/spec/models/stat_spec.rb index 535d64cc..72999b5b 100644 --- a/spec/models/stat_spec.rb +++ b/spec/models/stat_spec.rb @@ -11,9 +11,10 @@ RSpec.describe Stat, type: :model do describe 'methods' do let(:year) { 2021 } + let(:user) { create(:user) } describe '.year_cities_and_countries' do - subject { described_class.year_cities_and_countries(year) } + subject { described_class.year_cities_and_countries(year, user) } let(:timestamp) { DateTime.new(year, 1, 1, 0, 0, 0) } @@ -24,16 +25,16 @@ RSpec.describe Stat, type: :model do context 'when there are points' do let!(:points) do [ - create(:point, city: 'Berlin', country: 'Germany', timestamp:), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 10.minutes), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 20.minutes), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 30.minutes), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 40.minutes), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 50.minutes), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 60.minutes), - create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 70.minutes), - create(:point, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 80.minutes), - create(:point, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 90.minutes) + 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 @@ -86,8 +87,8 @@ RSpec.describe Stat, type: :model do 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)) + create(:point, user:, latitude: 1, longitude: 1, timestamp: DateTime.new(year, 1, 1, 1)) + create(:point, user:, latitude: 2, longitude: 2, timestamp: DateTime.new(year, 1, 1, 2)) end before { expected_distance[0][1] = 157.23 } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 3407d6e0..4098ad6c 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -7,6 +7,7 @@ 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) } + it { is_expected.to have_many(:tracked_points).class_name('Point').dependent(:destroy) } end describe 'callbacks' do diff --git a/spec/requests/points_spec.rb b/spec/requests/points_spec.rb index 1e583c56..b65f66c5 100644 --- a/spec/requests/points_spec.rb +++ b/spec/requests/points_spec.rb @@ -30,11 +30,12 @@ RSpec.describe '/points', type: :request do end describe 'DELETE /bulk_destroy' do - let(:point1) { create(:point) } - let(:point2) { create(:point) } + let(:user) { create(:user) } + let(:point1) { create(:point, user:) } + let(:point2) { create(:point, user:) } before do - sign_in create(:user) + sign_in user end it 'destroys the selected points' do diff --git a/swagger/v1/swagger.yaml b/swagger/v1/swagger.yaml index 530ad1c2..718e55fe 100644 --- a/swagger/v1/swagger.yaml +++ b/swagger/v1/swagger.yaml @@ -180,7 +180,7 @@ paths: lat: 52.502397 lon: 13.356718 tid: Swagger - tst: 1716634644 + tst: 1716636414 servers: - url: http://{defaultHost} variables: