mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Compare commits
8 commits
f79722f0e9
...
9874a87529
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9874a87529 | ||
|
|
699504f4e9 | ||
|
|
878d863569 | ||
|
|
24378b150d | ||
|
|
d2e2e50298 | ||
|
|
7885374993 | ||
|
|
244fb2b192 | ||
|
|
418df71c53 |
24 changed files with 451 additions and 121 deletions
33
CHANGELOG.md
33
CHANGELOG.md
|
|
@ -19,6 +19,39 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||
- Notification about Photon API load is now disabled.
|
||||
- All distance values are now stored in the database in meters. Conversion to user's preferred unit is done on the fly.
|
||||
- Every night, Dawarich will try to fetch names for places and visits that don't have them. #1281 #902 #583 #212
|
||||
- ⚠️ User settings are now being serialized in a more consistent way ⚠. `GET /api/v1/users/me` now returns the following data structure:
|
||||
```json
|
||||
{
|
||||
"user": {
|
||||
"email": "test@example.com",
|
||||
"theme": "light",
|
||||
"created_at": "2025-01-01T00:00:00Z",
|
||||
"updated_at": "2025-01-01T00:00:00Z",
|
||||
"settings": {
|
||||
"maps": {
|
||||
"url": "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
"name": "Custom OpenStreetMap",
|
||||
"distance_unit": "km"
|
||||
},
|
||||
"fog_of_war_meters": 51,
|
||||
"meters_between_routes": 500,
|
||||
"preferred_map_layer": "Light",
|
||||
"speed_colored_routes": false,
|
||||
"points_rendering_mode": "raw",
|
||||
"minutes_between_routes": 30,
|
||||
"time_threshold_minutes": 30,
|
||||
"merge_threshold_minutes": 15,
|
||||
"live_map_enabled": false,
|
||||
"route_opacity": 0.3,
|
||||
"immich_url": "https://persistence-test-1752264458724.com",
|
||||
"photoprism_url": "",
|
||||
"visits_suggestions_enabled": true,
|
||||
"speed_color_scale": "0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300",
|
||||
"fog_of_war_threshold": 5
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Fixed
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,6 @@
|
|||
|
||||
class Api::V1::UsersController < ApiController
|
||||
def me
|
||||
render json: { user: current_api_user }
|
||||
render json: Api::UserSerializer.new(current_api_user).call
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,27 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# This job is being run on daily basis to create tracks for all users
|
||||
# for the past 24 hours.
|
||||
# This job is being run on daily basis to create tracks for all users.
|
||||
# For each user, it starts from the end of their last track (or from their oldest point
|
||||
# if no tracks exist) and processes points until the specified end_at time.
|
||||
#
|
||||
# To manually run for a specific time range:
|
||||
# Tracks::BulkCreatingJob.perform_later(start_at: 1.week.ago, end_at: Time.current)
|
||||
#
|
||||
# To run for specific users only:
|
||||
# Tracks::BulkCreatingJob.perform_later(user_ids: [1, 2, 3])
|
||||
#
|
||||
# To let the job determine start times automatically (recommended):
|
||||
# Tracks::BulkCreatingJob.perform_later(end_at: Time.current)
|
||||
class Tracks::BulkCreatingJob < ApplicationJob
|
||||
queue_as :tracks
|
||||
sidekiq_options retry: false
|
||||
|
||||
def perform(start_at: 1.day.ago.beginning_of_day, end_at: 1.day.ago.end_of_day, user_ids: [])
|
||||
users = user_ids.any? ? User.active.where(id: user_ids) : User.active
|
||||
start_at = start_at.to_datetime
|
||||
end_at = end_at.to_datetime
|
||||
|
||||
users.find_each do |user|
|
||||
next if user.tracked_points.empty?
|
||||
next unless user.tracked_points.where(timestamp: start_at.to_i..end_at.to_i).exists?
|
||||
|
||||
Tracks::CreateJob.perform_later(user.id, start_at: start_at, end_at: end_at, cleaning_strategy: :daily)
|
||||
end
|
||||
def perform(start_at: nil, end_at: 1.day.ago.end_of_day, user_ids: [])
|
||||
Tracks::BulkTrackCreator.new(start_at:, end_at:, user_ids:).call
|
||||
end
|
||||
end
|
||||
|
|
|
|||
44
app/serializers/api/user_serializer.rb
Normal file
44
app/serializers/api/user_serializer.rb
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::UserSerializer
|
||||
def initialize(user)
|
||||
@user = user
|
||||
end
|
||||
|
||||
def call
|
||||
{
|
||||
user: {
|
||||
email: user.email,
|
||||
theme: user.theme,
|
||||
created_at: user.created_at,
|
||||
updated_at: user.updated_at,
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :user
|
||||
|
||||
def settings
|
||||
{
|
||||
maps: user.safe_settings.maps,
|
||||
fog_of_war_meters: user.safe_settings.fog_of_war_meters.to_i,
|
||||
meters_between_routes: user.safe_settings.meters_between_routes.to_i,
|
||||
preferred_map_layer: user.safe_settings.preferred_map_layer,
|
||||
speed_colored_routes: user.safe_settings.speed_colored_routes,
|
||||
points_rendering_mode: user.safe_settings.points_rendering_mode,
|
||||
minutes_between_routes: user.safe_settings.minutes_between_routes.to_i,
|
||||
time_threshold_minutes: user.safe_settings.time_threshold_minutes.to_i,
|
||||
merge_threshold_minutes: user.safe_settings.merge_threshold_minutes.to_i,
|
||||
live_map_enabled: user.safe_settings.live_map_enabled,
|
||||
route_opacity: user.safe_settings.route_opacity.to_f,
|
||||
immich_url: user.safe_settings.immich_url,
|
||||
photoprism_url: user.safe_settings.photoprism_url,
|
||||
visits_suggestions_enabled: user.safe_settings.visits_suggestions_enabled?,
|
||||
speed_color_scale: user.safe_settings.speed_color_scale,
|
||||
fog_of_war_threshold: user.safe_settings.fog_of_war_threshold
|
||||
}
|
||||
end
|
||||
end
|
||||
47
app/services/tracks/bulk_track_creator.rb
Normal file
47
app/services/tracks/bulk_track_creator.rb
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Tracks
|
||||
class BulkTrackCreator
|
||||
def initialize(start_at: nil, end_at: 1.day.ago.end_of_day, user_ids: [])
|
||||
@start_at = start_at&.to_datetime
|
||||
@end_at = end_at&.to_datetime
|
||||
@user_ids = user_ids
|
||||
end
|
||||
|
||||
def call
|
||||
users.find_each do |user|
|
||||
next if user.tracked_points.empty?
|
||||
|
||||
user_start_at = start_at || start_time(user)
|
||||
|
||||
next unless user.tracked_points.where(timestamp: user_start_at.to_i..end_at.to_i).exists?
|
||||
|
||||
Tracks::CreateJob.perform_later(
|
||||
user.id,
|
||||
start_at: user_start_at,
|
||||
end_at:,
|
||||
cleaning_strategy: :daily
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
attr_reader :start_at, :end_at, :user_ids
|
||||
|
||||
def users
|
||||
user_ids.any? ? User.active.where(id: user_ids) : User.active
|
||||
end
|
||||
|
||||
def start_time(user)
|
||||
latest_track = user.tracks.order(end_at: :desc).first
|
||||
|
||||
if latest_track
|
||||
latest_track.end_at
|
||||
else
|
||||
oldest_point = user.tracked_points.order(:timestamp).first
|
||||
oldest_point ? Time.zone.at(oldest_point.timestamp) : 1.day.ago.beginning_of_day
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -45,7 +45,9 @@ class Users::SafeSettings
|
|||
photoprism_api_key: photoprism_api_key,
|
||||
maps: maps,
|
||||
distance_unit: distance_unit,
|
||||
visits_suggestions_enabled: visits_suggestions_enabled?
|
||||
visits_suggestions_enabled: visits_suggestions_enabled?,
|
||||
speed_color_scale: speed_color_scale,
|
||||
fog_of_war_threshold: fog_of_war_threshold
|
||||
}
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
|
@ -118,4 +120,12 @@ class Users::SafeSettings
|
|||
def visits_suggestions_enabled?
|
||||
settings['visits_suggestions_enabled'] == 'true'
|
||||
end
|
||||
|
||||
def speed_color_scale
|
||||
settings['speed_color_scale']
|
||||
end
|
||||
|
||||
def fog_of_war_threshold
|
||||
settings['fog_of_war_threshold']
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Visits::Suggest
|
||||
include Rails.application.routes.url_helpers
|
||||
|
||||
attr_reader :points, :user, :start_at, :end_at
|
||||
|
||||
def initialize(user, start_at:, end_at:)
|
||||
|
|
@ -14,6 +12,7 @@ class Visits::Suggest
|
|||
|
||||
def call
|
||||
visits = Visits::SmartDetect.new(user, start_at:, end_at:).call
|
||||
|
||||
create_visits_notification(user) if visits.any?
|
||||
|
||||
return nil unless DawarichSettings.reverse_geocoding_enabled?
|
||||
|
|
@ -35,7 +34,7 @@ class Visits::Suggest
|
|||
|
||||
def create_visits_notification(user)
|
||||
content = <<~CONTENT
|
||||
New visits have been suggested based on your location data from #{Time.zone.at(start_at)} to #{Time.zone.at(end_at)}. You can review them on the <a href="#{visits_path}" class="link">Visits</a> page.
|
||||
New visits have been suggested based on your location data from #{Time.zone.at(start_at)} to #{Time.zone.at(end_at)}. You can review them on the <a href="/visits" class="link">Visits</a> page.
|
||||
CONTENT
|
||||
|
||||
user.notifications.create!(
|
||||
|
|
|
|||
|
|
@ -4,69 +4,16 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe Tracks::BulkCreatingJob, type: :job do
|
||||
describe '#perform' do
|
||||
let!(:active_user) { create(:user) }
|
||||
let!(:inactive_user) { create(:user, :inactive) }
|
||||
let!(:user_without_points) { create(:user) }
|
||||
|
||||
let(:start_at) { 1.day.ago.beginning_of_day }
|
||||
let(:end_at) { 1.day.ago.end_of_day }
|
||||
let(:service) { instance_double(Tracks::BulkTrackCreator) }
|
||||
|
||||
before do
|
||||
# Create points for active user in the target timeframe
|
||||
create(:point, user: active_user, timestamp: start_at.to_i + 1.hour.to_i)
|
||||
create(:point, user: active_user, timestamp: start_at.to_i + 2.hours.to_i)
|
||||
|
||||
# Create points for inactive user in the target timeframe
|
||||
create(:point, user: inactive_user, timestamp: start_at.to_i + 1.hour.to_i)
|
||||
allow(Tracks::BulkTrackCreator).to receive(:new).with(start_at: 'foo', end_at: 'bar', user_ids: [1, 2]).and_return(service)
|
||||
end
|
||||
|
||||
it 'schedules tracks creation jobs for active users with points in the timeframe' do
|
||||
expect {
|
||||
described_class.new.perform(start_at: start_at, end_at: end_at)
|
||||
}.to have_enqueued_job(Tracks::CreateJob).with(active_user.id, start_at: start_at, end_at: end_at, cleaning_strategy: :daily)
|
||||
end
|
||||
it 'calls Tracks::BulkTrackCreator with the correct arguments' do
|
||||
expect(service).to receive(:call)
|
||||
|
||||
it 'does not schedule jobs for users without tracked points' do
|
||||
expect {
|
||||
described_class.new.perform(start_at: start_at, end_at: end_at)
|
||||
}.not_to have_enqueued_job(Tracks::CreateJob).with(user_without_points.id, start_at: start_at, end_at: end_at, cleaning_strategy: :daily)
|
||||
end
|
||||
|
||||
it 'does not schedule jobs for users without points in the specified timeframe' do
|
||||
# Create a user with points outside the timeframe
|
||||
user_with_old_points = create(:user)
|
||||
create(:point, user: user_with_old_points, timestamp: 2.days.ago.to_i)
|
||||
|
||||
expect {
|
||||
described_class.new.perform(start_at: start_at, end_at: end_at)
|
||||
}.not_to have_enqueued_job(Tracks::CreateJob).with(user_with_old_points.id, start_at: start_at, end_at: end_at, cleaning_strategy: :daily)
|
||||
end
|
||||
|
||||
context 'when specific user_ids are provided' do
|
||||
it 'only processes the specified users' do
|
||||
expect {
|
||||
described_class.new.perform(start_at: start_at, end_at: end_at, user_ids: [active_user.id])
|
||||
}.to have_enqueued_job(Tracks::CreateJob).with(active_user.id, start_at: start_at, end_at: end_at, cleaning_strategy: :daily)
|
||||
end
|
||||
|
||||
it 'does not process users not in the user_ids list' do
|
||||
expect {
|
||||
described_class.new.perform(start_at: start_at, end_at: end_at, user_ids: [active_user.id])
|
||||
}.not_to have_enqueued_job(Tracks::CreateJob).with(inactive_user.id, start_at: start_at, end_at: end_at, cleaning_strategy: :daily)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with default parameters' do
|
||||
it 'uses yesterday as the default timeframe' do
|
||||
expect {
|
||||
described_class.new.perform
|
||||
}.to have_enqueued_job(Tracks::CreateJob).with(
|
||||
active_user.id,
|
||||
start_at: 1.day.ago.beginning_of_day.to_datetime,
|
||||
end_at: 1.day.ago.end_of_day.to_datetime,
|
||||
cleaning_strategy: :daily
|
||||
)
|
||||
end
|
||||
described_class.new.perform(start_at: 'foo', end_at: 'bar', user_ids: [1, 2])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -7,12 +7,28 @@ RSpec.describe 'Api::V1::Users', type: :request do
|
|||
let(:user) { create(:user) }
|
||||
let(:headers) { { 'Authorization' => "Bearer #{user.api_key}" } }
|
||||
|
||||
it 'returns http success' do
|
||||
it 'returns success response' do
|
||||
get '/api/v1/users/me', headers: headers
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response.body).to include(user.email)
|
||||
expect(response.body).to include(user.id.to_s)
|
||||
end
|
||||
|
||||
it 'returns only the keys and values stated in the serializer' do
|
||||
get '/api/v1/users/me', headers: headers
|
||||
|
||||
json = JSON.parse(response.body, symbolize_names: true)
|
||||
|
||||
expect(json.keys).to eq([:user])
|
||||
expect(json[:user].keys).to match_array(
|
||||
%i[email theme created_at updated_at settings]
|
||||
)
|
||||
expect(json[:user][:settings].keys).to match_array(%i[
|
||||
maps fog_of_war_meters meters_between_routes preferred_map_layer
|
||||
speed_colored_routes points_rendering_mode minutes_between_routes
|
||||
time_threshold_minutes merge_threshold_minutes live_map_enabled
|
||||
route_opacity immich_url photoprism_url visits_suggestions_enabled
|
||||
speed_color_scale fog_of_war_threshold
|
||||
])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
85
spec/serializers/api/user_serializer_spec.rb
Normal file
85
spec/serializers/api/user_serializer_spec.rb
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Api::UserSerializer do
|
||||
describe '#call' do
|
||||
subject(:serializer) { described_class.new(user).call }
|
||||
|
||||
let(:user) { create(:user, email: 'test@example.com', theme: 'dark') }
|
||||
|
||||
it 'returns JSON with correct user attributes' do
|
||||
expect(serializer[:user][:email]).to eq(user.email)
|
||||
expect(serializer[:user][:theme]).to eq(user.theme)
|
||||
expect(serializer[:user][:created_at]).to eq(user.created_at)
|
||||
expect(serializer[:user][:updated_at]).to eq(user.updated_at)
|
||||
end
|
||||
|
||||
it 'returns settings with expected keys and types' do
|
||||
settings = serializer[:user][:settings]
|
||||
expect(settings).to include(
|
||||
:maps,
|
||||
:fog_of_war_meters,
|
||||
:meters_between_routes,
|
||||
:preferred_map_layer,
|
||||
:speed_colored_routes,
|
||||
:points_rendering_mode,
|
||||
:minutes_between_routes,
|
||||
:time_threshold_minutes,
|
||||
:merge_threshold_minutes,
|
||||
:live_map_enabled,
|
||||
:route_opacity,
|
||||
:immich_url,
|
||||
:photoprism_url,
|
||||
:visits_suggestions_enabled,
|
||||
:speed_color_scale,
|
||||
:fog_of_war_threshold
|
||||
)
|
||||
end
|
||||
|
||||
context 'with custom settings' do
|
||||
let(:custom_settings) do
|
||||
{
|
||||
'fog_of_war_meters' => 123,
|
||||
'meters_between_routes' => 456,
|
||||
'preferred_map_layer' => 'Satellite',
|
||||
'speed_colored_routes' => true,
|
||||
'points_rendering_mode' => 'cluster',
|
||||
'minutes_between_routes' => 42,
|
||||
'time_threshold_minutes' => 99,
|
||||
'merge_threshold_minutes' => 77,
|
||||
'live_map_enabled' => false,
|
||||
'route_opacity' => 0.75,
|
||||
'immich_url' => 'https://immich.example.com',
|
||||
'photoprism_url' => 'https://photoprism.example.com',
|
||||
'visits_suggestions_enabled' => 'false',
|
||||
'speed_color_scale' => 'rainbow',
|
||||
'fog_of_war_threshold' => 5,
|
||||
'maps' => { 'distance_unit' => 'mi' }
|
||||
}
|
||||
end
|
||||
|
||||
let(:user) { create(:user, settings: custom_settings) }
|
||||
|
||||
it 'serializes custom settings correctly' do
|
||||
settings = serializer[:user][:settings]
|
||||
expect(settings[:fog_of_war_meters]).to eq(123)
|
||||
expect(settings[:meters_between_routes]).to eq(456)
|
||||
expect(settings[:preferred_map_layer]).to eq('Satellite')
|
||||
expect(settings[:speed_colored_routes]).to eq(true)
|
||||
expect(settings[:points_rendering_mode]).to eq('cluster')
|
||||
expect(settings[:minutes_between_routes]).to eq(42)
|
||||
expect(settings[:time_threshold_minutes]).to eq(99)
|
||||
expect(settings[:merge_threshold_minutes]).to eq(77)
|
||||
expect(settings[:live_map_enabled]).to eq(false)
|
||||
expect(settings[:route_opacity]).to eq(0.75)
|
||||
expect(settings[:immich_url]).to eq('https://immich.example.com')
|
||||
expect(settings[:photoprism_url]).to eq('https://photoprism.example.com')
|
||||
expect(settings[:visits_suggestions_enabled]).to eq(false)
|
||||
expect(settings[:speed_color_scale]).to eq('rainbow')
|
||||
expect(settings[:fog_of_war_threshold]).to eq(5)
|
||||
expect(settings[:maps]).to eq({ 'distance_unit' => 'mi' })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
176
spec/services/tracks/bulk_track_creator_spec.rb
Normal file
176
spec/services/tracks/bulk_track_creator_spec.rb
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Tracks::BulkTrackCreator do
|
||||
describe '#call' do
|
||||
let!(:active_user) { create(:user) }
|
||||
let!(:inactive_user) { create(:user, :inactive) }
|
||||
let!(:user_without_points) { create(:user) }
|
||||
|
||||
let(:start_at) { 1.day.ago.beginning_of_day }
|
||||
let(:end_at) { 1.day.ago.end_of_day }
|
||||
|
||||
before do
|
||||
# Create points for active user in the target timeframe
|
||||
create(:point, user: active_user, timestamp: start_at.to_i + 1.hour.to_i)
|
||||
create(:point, user: active_user, timestamp: start_at.to_i + 2.hours.to_i)
|
||||
|
||||
# Create points for inactive user in the target timeframe
|
||||
create(:point, user: inactive_user, timestamp: start_at.to_i + 1.hour.to_i)
|
||||
end
|
||||
|
||||
context 'when explicit start_at is provided' do
|
||||
it 'schedules tracks creation jobs for active users with points in the timeframe' do
|
||||
expect {
|
||||
described_class.new(start_at:, end_at:).call
|
||||
}.to have_enqueued_job(Tracks::CreateJob).with(active_user.id, start_at:, end_at:, cleaning_strategy: :daily)
|
||||
end
|
||||
|
||||
it 'does not schedule jobs for users without tracked points' do
|
||||
expect {
|
||||
described_class.new(start_at:, end_at:).call
|
||||
}.not_to have_enqueued_job(Tracks::CreateJob).with(user_without_points.id, start_at:, end_at:, cleaning_strategy: :daily)
|
||||
end
|
||||
|
||||
it 'does not schedule jobs for users without points in the specified timeframe' do
|
||||
# Create a user with points outside the timeframe
|
||||
user_with_old_points = create(:user)
|
||||
create(:point, user: user_with_old_points, timestamp: 2.days.ago.to_i)
|
||||
|
||||
expect {
|
||||
described_class.new(start_at:, end_at:).call
|
||||
}.not_to have_enqueued_job(Tracks::CreateJob).with(user_with_old_points.id, start_at:, end_at:, cleaning_strategy: :daily)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when specific user_ids are provided' do
|
||||
it 'only processes the specified users' do
|
||||
expect {
|
||||
described_class.new(start_at:, end_at:, user_ids: [active_user.id]).call
|
||||
}.to have_enqueued_job(Tracks::CreateJob).with(active_user.id, start_at:, end_at:, cleaning_strategy: :daily)
|
||||
end
|
||||
|
||||
it 'does not process users not in the user_ids list' do
|
||||
expect {
|
||||
described_class.new(start_at:, end_at:, user_ids: [active_user.id]).call
|
||||
}.not_to have_enqueued_job(Tracks::CreateJob).with(inactive_user.id, start_at:, end_at:, cleaning_strategy: :daily)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with automatic start time determination' do
|
||||
let(:user_with_tracks) { create(:user) }
|
||||
let(:user_without_tracks) { create(:user) }
|
||||
let(:current_time) { Time.current }
|
||||
|
||||
before do
|
||||
# Create some historical points and tracks for user_with_tracks
|
||||
create(:point, user: user_with_tracks, timestamp: 3.days.ago.to_i)
|
||||
create(:point, user: user_with_tracks, timestamp: 2.days.ago.to_i)
|
||||
|
||||
# Create a track ending 1 day ago
|
||||
create(:track, user: user_with_tracks, end_at: 1.day.ago)
|
||||
|
||||
# Create newer points after the last track
|
||||
create(:point, user: user_with_tracks, timestamp: 12.hours.ago.to_i)
|
||||
create(:point, user: user_with_tracks, timestamp: 6.hours.ago.to_i)
|
||||
|
||||
# Create points for user without tracks
|
||||
create(:point, user: user_without_tracks, timestamp: 2.days.ago.to_i)
|
||||
create(:point, user: user_without_tracks, timestamp: 1.day.ago.to_i)
|
||||
end
|
||||
|
||||
it 'starts from the end of the last track for users with existing tracks' do
|
||||
track_end_time = user_with_tracks.tracks.order(end_at: :desc).first.end_at
|
||||
|
||||
expect {
|
||||
described_class.new(end_at: current_time, user_ids: [user_with_tracks.id]).call
|
||||
}.to have_enqueued_job(Tracks::CreateJob).with(
|
||||
user_with_tracks.id,
|
||||
start_at: track_end_time,
|
||||
end_at: current_time.to_datetime,
|
||||
cleaning_strategy: :daily
|
||||
)
|
||||
end
|
||||
|
||||
it 'starts from the oldest point for users without tracks' do
|
||||
oldest_point_time = Time.zone.at(user_without_tracks.tracked_points.order(:timestamp).first.timestamp)
|
||||
|
||||
expect {
|
||||
described_class.new(end_at: current_time, user_ids: [user_without_tracks.id]).call
|
||||
}.to have_enqueued_job(Tracks::CreateJob).with(
|
||||
user_without_tracks.id,
|
||||
start_at: oldest_point_time,
|
||||
end_at: current_time.to_datetime,
|
||||
cleaning_strategy: :daily
|
||||
)
|
||||
end
|
||||
|
||||
it 'falls back to 1 day ago for users with no points' do
|
||||
expect {
|
||||
described_class.new(end_at: current_time, user_ids: [user_without_points.id]).call
|
||||
}.not_to have_enqueued_job(Tracks::CreateJob).with(
|
||||
user_without_points.id,
|
||||
start_at: anything,
|
||||
end_at: anything,
|
||||
cleaning_strategy: :daily
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with default parameters' do
|
||||
let(:user_with_recent_points) { create(:user) }
|
||||
|
||||
before do
|
||||
# Create points within yesterday's timeframe
|
||||
create(:point, user: user_with_recent_points, timestamp: 1.day.ago.beginning_of_day.to_i + 2.hours.to_i)
|
||||
create(:point, user: user_with_recent_points, timestamp: 1.day.ago.beginning_of_day.to_i + 6.hours.to_i)
|
||||
end
|
||||
|
||||
it 'uses automatic start time determination with yesterday as end_at' do
|
||||
oldest_point_time = Time.zone.at(user_with_recent_points.tracked_points.order(:timestamp).first.timestamp)
|
||||
|
||||
expect {
|
||||
described_class.new(user_ids: [user_with_recent_points.id]).call
|
||||
}.to have_enqueued_job(Tracks::CreateJob).with(
|
||||
user_with_recent_points.id,
|
||||
start_at: oldest_point_time,
|
||||
end_at: 1.day.ago.end_of_day.to_datetime,
|
||||
cleaning_strategy: :daily
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#start_time' do
|
||||
let(:user) { create(:user) }
|
||||
let(:service) { described_class.new }
|
||||
|
||||
context 'when user has tracks' do
|
||||
let!(:old_track) { create(:track, user: user, end_at: 3.days.ago) }
|
||||
let!(:recent_track) { create(:track, user: user, end_at: 1.day.ago) }
|
||||
|
||||
it 'returns the end time of the most recent track' do
|
||||
result = service.send(:start_time, user)
|
||||
expect(result).to eq(recent_track.end_at)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has no tracks but has points' do
|
||||
let!(:old_point) { create(:point, user: user, timestamp: 5.days.ago.to_i) }
|
||||
let!(:recent_point) { create(:point, user: user, timestamp: 2.days.ago.to_i) }
|
||||
|
||||
it 'returns the timestamp of the oldest point' do
|
||||
result = service.send(:start_time, user)
|
||||
expect(result).to eq(Time.zone.at(old_point.timestamp))
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user has no tracks and no points' do
|
||||
it 'returns 1 day ago beginning of day' do
|
||||
result = service.send(:start_time, user)
|
||||
expect(result).to eq(1.day.ago.beginning_of_day)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -27,7 +27,9 @@ RSpec.describe Users::SafeSettings do
|
|||
photoprism_api_key: nil,
|
||||
maps: { "distance_unit" => "km" },
|
||||
distance_unit: 'km',
|
||||
visits_suggestions_enabled: true
|
||||
visits_suggestions_enabled: true,
|
||||
speed_color_scale: nil,
|
||||
fog_of_war_threshold: nil
|
||||
}
|
||||
)
|
||||
end
|
||||
|
|
@ -98,7 +100,9 @@ RSpec.describe Users::SafeSettings do
|
|||
photoprism_api_key: "photoprism-key",
|
||||
maps: { "name" => "custom", "url" => "https://custom.example.com" },
|
||||
distance_unit: nil,
|
||||
visits_suggestions_enabled: false
|
||||
visits_suggestions_enabled: false,
|
||||
speed_color_scale: nil,
|
||||
fog_of_war_threshold: nil
|
||||
}
|
||||
)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ RSpec.describe Visits::Suggest do
|
|||
|
||||
before do
|
||||
allow(DawarichSettings).to receive(:reverse_geocoding_enabled?).and_return(true)
|
||||
# Create points for reverse geocoding test in a separate time range
|
||||
|
||||
create_visit_points(user, reverse_geocoding_start_at)
|
||||
clear_enqueued_jobs
|
||||
end
|
||||
|
|
|
|||
|
|
@ -29,19 +29,22 @@ describe 'Users API', type: :request do
|
|||
settings: {
|
||||
type: :object,
|
||||
properties: {
|
||||
immich_url: { type: :string },
|
||||
route_opacity: { type: :string },
|
||||
immich_api_key: { type: :string },
|
||||
live_map_enabled: { type: :boolean },
|
||||
fog_of_war_meters: { type: :string },
|
||||
maps: { type: :object },
|
||||
fog_of_war_meters: { type: :integer },
|
||||
meters_between_routes: { type: :integer },
|
||||
preferred_map_layer: { type: :string },
|
||||
speed_colored_routes: { type: :boolean },
|
||||
meters_between_routes: { type: :string },
|
||||
points_rendering_mode: { type: :string },
|
||||
minutes_between_routes: { type: :string },
|
||||
time_threshold_minutes: { type: :string },
|
||||
merge_threshold_minutes: { type: :string },
|
||||
speed_colored_polylines: { type: :boolean }
|
||||
minutes_between_routes: { type: :integer },
|
||||
time_threshold_minutes: { type: :integer },
|
||||
merge_threshold_minutes: { type: :integer },
|
||||
live_map_enabled: { type: :boolean },
|
||||
route_opacity: { type: :number },
|
||||
immich_url: { type: :string, nullable: true },
|
||||
photoprism_url: { type: :string, nullable: true },
|
||||
visits_suggestions_enabled: { type: :boolean },
|
||||
speed_color_scale: { type: :string, nullable: true },
|
||||
fog_of_war_threshold: { type: :string, nullable: true }
|
||||
}
|
||||
},
|
||||
admin: { type: :boolean }
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
|
||||
driven_by :selenium, using: :chrome, screen_size: [1400, 1400]
|
||||
end
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
require "test_helper"
|
||||
|
||||
class ApplicationCable::ConnectionTest < ActionCable::Connection::TestCase
|
||||
# test "connects with cookies" do
|
||||
# cookies.signed[:user_id] = 42
|
||||
#
|
||||
# connect
|
||||
#
|
||||
# assert_equal connection.user_id, "42"
|
||||
# end
|
||||
end
|
||||
0
test/fixtures/files/.keep
vendored
0
test/fixtures/files/.keep
vendored
|
|
@ -1,13 +0,0 @@
|
|||
ENV["RAILS_ENV"] ||= "test"
|
||||
require_relative "../config/environment"
|
||||
require "rails/test_help"
|
||||
|
||||
class ActiveSupport::TestCase
|
||||
# Run tests in parallel with specified workers
|
||||
parallelize(workers: :number_of_processors)
|
||||
|
||||
# Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order.
|
||||
fixtures :all
|
||||
|
||||
# Add more helper methods to be used by all tests here...
|
||||
end
|
||||
Loading…
Reference in a new issue