Enable visit suggesting job

This commit is contained in:
Eugene Burmakin 2025-03-09 20:07:39 +01:00
parent 9a4a6481d0
commit b8e6b1a372
8 changed files with 131 additions and 40 deletions

View file

@ -1 +1 @@
0.24.2
0.25.0

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -39,9 +39,9 @@
<tr>
<td><%= place.name %></td>
<td><%= place.created_at.strftime('%Y-%m-%d %H:%M:%S') %></td>
<td><%= place.to_coordinates.map(&:to_f) %></td>
<td><%= "#{place.lat}, #{place.lon}" %></td>
<td>
<%= 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" %>
</td>
</tr>
<% end %>

View file

@ -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

1
db/schema.rb generated
View file

@ -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

View file

@ -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