diff --git a/.app_version b/.app_version index 8b95abd9..d21d277b 100644 --- a/.app_version +++ b/.app_version @@ -1 +1 @@ -0.24.2 +0.25.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index ec16e47e..ada5cca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,15 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). -TODO: - -- Specs for app/services/visits/merge_service.rb and rename it probably -- Remove Stimulus controllers for visits on the Visits page -- Revert changes to Visits page -- Decide on how to suggest visits for the past -- Should visits be disabled for non-reverse-geocoded instances? - -# 0.25.0 - 2025-03-08 +# 0.25.0 - 2025-03-09 This release is focused on improving the visits experience. @@ -26,29 +18,12 @@ This release is focused on improving the visits experience. - User can select click on the "Select area" button in the top right corner of the map to select an area on the map. Once area is selected, visits for all times in that area will be shown on the map, regardless of whether they are in the selected time range or not. - User can now select two or more visits in the visits drawer and merge them into a single visit. This operation is not reversible. - User can now select two or more visits in the visits drawer and confirm or decline them at once. This operation is not reversible. +- Status field to the User model. Inactive users are now being restricted from accessing some of the functionality, which is mostly about writing data to the database. Reading is remaining unrestricted. + ## Changed - Links to Points, Visits & Places, Imports and Exports were moved under "My data" section in the navbar. - -## Fixed - - - -# 0.24.2 - 2025-02-24 - -## Added - -- Status field to the User model. Inactive users are now being restricted from accessing some of the functionality, which is mostly about writing data to the database. Reading is remaining unrestricted. - -## Fixed - -- Fixed a bug where non-admin users could not import Immich and Photoprism geolocation data. -- Fixed a bug where upon point deletion it was not being removed from the map, while it was actually deleted from the database. #883 -- Fixed a bug where upon import deletion stats were not being recalculated. #824 - -### Changed - - Restrict access to Sidekiq in non self-hosted mode. - Restrict access to background jobs in non self-hosted mode. - Restrict access to users management in non self-hosted mode. @@ -57,6 +32,12 @@ This release is focused on improving the visits experience. - GPX files are now being imported much faster. - Distance calculation are now using Postgis functions and expected to be more accurate. +## Fixed + +- Fixed a bug where non-admin users could not import Immich and Photoprism geolocation data. +- Fixed a bug where upon point deletion it was not being removed from the map, while it was actually deleted from the database. #883 +- Fixed a bug where upon import deletion stats were not being recalculated. #824 + # 0.24.1 - 2025-02-13 ## Custom map tiles diff --git a/app/jobs/bulk_visits_suggesting_job.rb b/app/jobs/bulk_visits_suggesting_job.rb index 1d615609..54174bca 100644 --- a/app/jobs/bulk_visits_suggesting_job.rb +++ b/app/jobs/bulk_visits_suggesting_job.rb @@ -1,12 +1,16 @@ # frozen_string_literal: true +# This job is being run on daily basis at 00:05 to suggest visits for all users +# with the default timespan of 1 day. class BulkVisitsSuggestingJob < ApplicationJob - queue_as :default + queue_as :visit_suggesting sidekiq_options retry: false # Passing timespan of more than 3 years somehow results in duplicated Places - def perform(start_at:, end_at:, user_ids: []) - users = user_ids.any? ? User.where(id: user_ids) : User.all + def perform(start_at: 1.day.ago.beginning_of_day, end_at: 1.day.ago.end_of_day, user_ids: []) + return unless DawarichSettings.reverse_geocoding_enabled? + + users = user_ids.any? ? User.active.where(id: user_ids) : User.active start_at = start_at.to_datetime end_at = end_at.to_datetime diff --git a/app/models/point.rb b/app/models/point.rb index 4de7fa36..38970d91 100644 --- a/app/models/point.rb +++ b/app/models/point.rb @@ -36,7 +36,7 @@ class Point < ApplicationRecord end def recorded_at - Time.zone.at(timestamp) + @recorded_at ||= Time.zone.at(timestamp) end def async_reverse_geocode diff --git a/app/views/places/index.html.erb b/app/views/places/index.html.erb index 939cca3b..5ed9365f 100644 --- a/app/views/places/index.html.erb +++ b/app/views/places/index.html.erb @@ -39,9 +39,9 @@ <%= place.name %> <%= place.created_at.strftime('%Y-%m-%d %H:%M:%S') %> - <%= place.to_coordinates.map(&:to_f) %> + <%= "#{place.lat}, #{place.lon}" %> - <%= link_to 'Delete', place, data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?", turbo_method: :delete }, method: :delete, class: "px-4 py-2 bg-red-500 text-white rounded-md" %> + <%= link_to 'Delete', place, data: { confirm: "Are you sure? Deleting a place will result in deleting all visits for this place.", turbo_confirm: "Are you sure? Deleting a place will result in deleting all visits for this place.", turbo_method: :delete }, method: :delete, class: "px-4 py-2 bg-red-500 text-white rounded-md" %> <% end %> diff --git a/config/schedule.yml b/config/schedule.yml index e27337d6..975d22b5 100644 --- a/config/schedule.yml +++ b/config/schedule.yml @@ -10,11 +10,10 @@ area_visits_calculation_scheduling_job: class: "AreaVisitsCalculationSchedulingJob" queue: visit_suggesting -# Disabled until fixed -# visit_suggesting_job: -# cron: "0 1 * * *" # every day at 1:00 -# class: "VisitSuggestingJob" -# queue: visit_suggesting +visit_suggesting_job: + cron: "5 0 * * *" # every day at 00:05 + class: "BulkVisitsSuggestingJob" + queue: visit_suggesting watcher_job: cron: "0 */1 * * *" # every 1 hour diff --git a/db/schema.rb b/db/schema.rb index 562e417c..73945456 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -126,6 +126,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_03_03_194043) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.geography "lonlat", limit: {srid: 4326, type: "st_point", geographic: true} + t.index "name, st_astext(lonlat)", name: "index_places_on_name_and_lonlat", unique: true t.index ["lonlat"], name: "index_places_on_lonlat", using: :gist end diff --git a/spec/jobs/bulk_visits_suggesting_job_spec.rb b/spec/jobs/bulk_visits_suggesting_job_spec.rb new file mode 100644 index 00000000..6c76c745 --- /dev/null +++ b/spec/jobs/bulk_visits_suggesting_job_spec.rb @@ -0,0 +1,106 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe BulkVisitsSuggestingJob, type: :job do + describe '#perform' do + let(:start_at) { 1.day.ago.beginning_of_day } + let(:end_at) { 1.day.ago.end_of_day } + let(:user) { create(:user) } + let(:inactive_user) { create(:user, status: :inactive) } + let(:user_with_points) { create(:user) } + let(:time_chunks) { [[start_at, end_at]] } + + before do + allow(DawarichSettings).to receive(:reverse_geocoding_enabled?).and_return(true) + allow_any_instance_of(Visits::TimeChunks).to receive(:call).and_return(time_chunks) + create(:point, user: user_with_points) + end + + it 'does nothing if reverse geocoding is disabled' do + allow(DawarichSettings).to receive(:reverse_geocoding_enabled?).and_return(false) + + expect(VisitSuggestingJob).not_to receive(:perform_later) + + described_class.perform_now + end + + it 'schedules jobs only for active users with tracked points' do + expect(VisitSuggestingJob).to receive(:perform_later).with( + user_id: user_with_points.id, + start_at: time_chunks.first.first, + end_at: time_chunks.first.last + ) + + expect(VisitSuggestingJob).not_to receive(:perform_later).with( + user_id: user.id, + start_at: anything, + end_at: anything + ) + + expect(VisitSuggestingJob).not_to receive(:perform_later).with( + user_id: inactive_user.id, + start_at: anything, + end_at: anything + ) + + described_class.perform_now + end + + it 'handles multiple time chunks' do + chunks = [ + [start_at, start_at + 12.hours], + [start_at + 12.hours, end_at] + ] + allow_any_instance_of(Visits::TimeChunks).to receive(:call).and_return(chunks) + + chunks.each do |chunk| + expect(VisitSuggestingJob).to receive(:perform_later).with( + user_id: user_with_points.id, + start_at: chunk.first, + end_at: chunk.last + ) + end + + described_class.perform_now + end + + it 'only processes specified users when user_ids is provided' do + create(:point, user: user) + + expect(VisitSuggestingJob).to receive(:perform_later).with( + user_id: user.id, + start_at: time_chunks.first.first, + end_at: time_chunks.first.last + ) + + expect(VisitSuggestingJob).not_to receive(:perform_later).with( + user_id: user_with_points.id, + start_at: anything, + end_at: anything + ) + + described_class.perform_now(user_ids: [user.id]) + end + + it 'uses custom time range when provided' do + custom_start = 2.days.ago.beginning_of_day + custom_end = 2.days.ago.end_of_day + custom_chunks = [[custom_start, custom_end]] + + time_chunks_instance = instance_double(Visits::TimeChunks) + allow(Visits::TimeChunks).to receive(:new) + .with(start_at: custom_start, end_at: custom_end) + .and_return(time_chunks_instance) + allow(time_chunks_instance).to receive(:call).and_return(custom_chunks) + + expect(VisitSuggestingJob).to receive(:perform_later).with( + user_id: user_with_points.id, + start_at: custom_chunks.first.first, + end_at: custom_chunks.first.last + ) + + described_class.perform_now(start_at: custom_start, end_at: custom_end) + end + end +end