Merge pull request #1552 from Freika/dev

0.30.1
This commit is contained in:
Evgenii Burmakin 2025-07-22 20:46:11 +02:00 committed by GitHub
commit abfd3be1c5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 317 additions and 42 deletions

View file

@ -1 +1 @@
0.30.0
0.30.1

View file

@ -4,10 +4,22 @@ 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/).
# [0.30.1] - 2025-07-22
## Fixed
- Points limit exceeded check is now cached.
- Reverse geocoding for places is now significantly faster.
## Changed
- Stats page should load faster now.
- Track creation is temporarily disabled.
# [0.30.0] - 2025-07-21
⚠️ If you were using RC, please run the following commands in the console, otherwise read on. ⚠️
⚠️ If you were using 0.29.2 RC, please run the following commands in the console, otherwise read on. ⚠️
```ruby
# This will delete all tracks 👇

View file

@ -126,7 +126,7 @@ GEM
rack-test (>= 0.6.3)
regexp_parser (>= 1.5, < 3.0)
xpath (~> 3.2)
chartkick (5.1.5)
chartkick (5.2.0)
coderay (1.1.3)
concurrent-ruby (1.3.5)
connection_pool (2.5.3)
@ -144,7 +144,7 @@ GEM
database_consistency (2.0.4)
activerecord (>= 3.2)
date (3.4.1)
debug (1.10.0)
debug (1.11.0)
irb (~> 1.10)
reline (>= 0.3.8)
devise (4.9.4)
@ -160,7 +160,7 @@ GEM
dotenv (= 3.1.8)
railties (>= 6.1)
drb (2.2.3)
erb (5.0.1)
erb (5.0.2)
erubi (1.13.1)
et-orbi (1.2.11)
tzinfo
@ -194,7 +194,7 @@ GEM
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
io-console (0.8.0)
io-console (0.8.1)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
@ -243,7 +243,7 @@ GEM
multi_json (1.15.0)
multi_xml (0.7.1)
bigdecimal (~> 3.1)
net-imap (0.5.8)
net-imap (0.5.9)
date
net-protocol
net-pop (0.1.2)
@ -253,23 +253,23 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.7.4)
nokogiri (1.18.8)
nokogiri (1.18.9)
mini_portile2 (~> 2.8.2)
racc (~> 1.4)
nokogiri (1.18.8-aarch64-linux-gnu)
nokogiri (1.18.9-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-arm-linux-gnu)
nokogiri (1.18.9-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.8-arm64-darwin)
nokogiri (1.18.9-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-darwin)
nokogiri (1.18.9-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.8-x86_64-linux-gnu)
nokogiri (1.18.9-x86_64-linux-gnu)
racc (~> 1.4)
oj (3.16.11)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
optimist (3.2.0)
optimist (3.2.1)
orm_adapter (0.5.0)
ostruct (0.6.1)
parallel (1.27.0)
@ -342,7 +342,7 @@ GEM
zeitwerk (~> 2.6)
rainbow (3.1.1)
rake (13.3.0)
rdoc (6.14.1)
rdoc (6.14.2)
erb
psych (>= 4.0.0)
redis (5.4.0)
@ -350,7 +350,7 @@ GEM
redis-client (0.24.0)
connection_pool
regexp_parser (2.10.0)
reline (0.6.1)
reline (0.6.2)
io-console (~> 0.5)
request_store (1.7.0)
rack (>= 1.4)
@ -462,7 +462,7 @@ GEM
stringio (3.1.7)
strong_migrations (2.3.0)
activerecord (>= 7)
super_diff (0.15.0)
super_diff (0.16.0)
attr_extras (>= 6.2.4)
diff-lcs
patience_diff
@ -475,7 +475,7 @@ GEM
tailwindcss-ruby (3.4.17-arm64-darwin)
tailwindcss-ruby (3.4.17-x86_64-darwin)
tailwindcss-ruby (3.4.17-x86_64-linux)
thor (1.3.2)
thor (1.4.0)
timeout (0.4.3)
turbo-rails (2.0.16)
actionpack (>= 7.1.0)
@ -496,7 +496,7 @@ GEM
hashdiff (>= 0.4.0, < 2.0.0)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.7.7)
websocket-driver (0.8.0)
base64
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)

View file

@ -5,10 +5,9 @@ class StatsController < ApplicationController
before_action :authenticate_active_user!, only: %i[update update_all]
def index
@stats = current_user.stats.group_by(&:year).transform_values { |stats| stats.sort_by(&:updated_at).reverse }.sort.reverse
@points_total = current_user.tracked_points.count
@points_reverse_geocoded = current_user.total_reverse_geocoded_points
@points_reverse_geocoded_without_data = current_user.total_reverse_geocoded_points_without_data
@stats = build_stats
assign_points_statistics
@year_distances = precompute_year_distances
end
def show
@ -43,4 +42,30 @@ class StatsController < ApplicationController
redirect_to stats_path, notice: 'Stats are being updated', status: :see_other
end
private
def assign_points_statistics
points_stats = ::StatsQuery.new(current_user).points_stats
@points_total = points_stats[:total]
@points_reverse_geocoded = points_stats[:geocoded]
@points_reverse_geocoded_without_data = points_stats[:without_data]
end
def precompute_year_distances
year_distances = {}
@stats.each do |year, _stats|
year_distances[year] = Stat.year_distance(year, current_user)
end
year_distances
end
def build_stats
current_user.stats.group_by(&:year).transform_values do |stats|
stats.sort_by(&:updated_at).reverse
end.sort.reverse
end
end

View file

@ -219,7 +219,7 @@ export default class extends BaseController {
this.setupTracksSubscription();
// Handle routes/tracks mode selection
this.addRoutesTracksSelector();
// this.addRoutesTracksSelector(); # Temporarily disabled
this.switchRouteMode('routes', true);
// Initialize layers based on settings

View file

@ -27,6 +27,8 @@ class Tracks::CreateJob < ApplicationJob
end
def create_error_notification(user, error)
return unless DawarichSettings.self_hosted?
Notifications::Create.new(
user: user,
kind: :error,

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Trips::CalculateAllJob < ApplicationJob
queue_as :default
queue_as :trips
def perform(trip_id, distance_unit = 'km')
Trips::CalculatePathJob.perform_later(trip_id)

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Trips::CalculateCountriesJob < ApplicationJob
queue_as :default
queue_as :trips
def perform(trip_id, distance_unit)
trip = Trip.find(trip_id)

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Trips::CalculateDistanceJob < ApplicationJob
queue_as :default
queue_as :trips
def perform(trip_id, distance_unit)
trip = Trip.find(trip_id)

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Trips::CalculatePathJob < ApplicationJob
queue_as :default
queue_as :trips
def perform(trip_id)
trip = Trip.find(trip_id)

View file

@ -12,6 +12,8 @@ class Country < ApplicationRecord
end
def self.names_to_iso_a2
pluck(:name, :iso_a2).to_h
Rails.cache.fetch('countries_names_to_iso_a2', expires_in: 1.day) do
pluck(:name, :iso_a2).to_h
end
end
end

View file

@ -33,8 +33,8 @@ class Point < ApplicationRecord
after_create :async_reverse_geocode, if: -> { DawarichSettings.store_geodata? && !reverse_geocoded? }
after_create :set_country
after_create_commit :broadcast_coordinates
after_create_commit :trigger_incremental_track_generation, if: -> { import_id.nil? }
after_commit :recalculate_track, on: :update, if: -> { track.present? }
# after_create_commit :trigger_incremental_track_generation, if: -> { import_id.nil? }
# after_commit :recalculate_track, on: :update, if: -> { track.present? }
def self.without_raw_data
select(column_names - ['raw_data'])

View file

@ -0,0 +1,33 @@
# frozen_string_literal: true
class StatsQuery
def initialize(user)
@user = user
end
def points_stats
sql = ActiveRecord::Base.sanitize_sql_array([
<<~SQL.squish,
SELECT
COUNT(id) as total,
COUNT(reverse_geocoded_at) as geocoded,
COUNT(CASE WHEN geodata = '{}'::jsonb THEN 1 END) as without_data
FROM points
WHERE user_id = ?
SQL
user.id
])
result = Point.connection.select_one(sql)
{
total: result['total'].to_i,
geocoded: result['geocoded'].to_i,
without_data: result['without_data'].to_i
}
end
private
attr_reader :user
end

View file

@ -7,13 +7,18 @@ class PointsLimitExceeded
def call
return false if DawarichSettings.self_hosted?
return true if @user.tracked_points.count >= points_limit
false
Rails.cache.fetch(cache_key, expires_in: 1.day) do
@user.tracked_points.count >= points_limit
end
end
private
def cache_key
"points_limit_exceeded/#{@user.id}"
end
def points_limit
DawarichSettings::BASIC_PAID_PLAN_LIMIT
end

View file

@ -4,7 +4,7 @@
</h2>
<div class='my-10'>
<%= column_chart(
Stat.year_distance(year, current_user),
@year_distances[year],
height: '200px',
suffix: " #{current_user.safe_settings.distance_unit}",
xtitle: 'Days',

View file

@ -82,7 +82,7 @@
</div>
<% end %>
<%= column_chart(
Stat.year_distance(year, current_user).map { |month_name, distance_meters|
@year_distances[year].map { |month_name, distance_meters|
[month_name, Stat.convert_distance(distance_meters, current_user.safe_settings.distance_unit).round(2)]
},
height: '200px',

View file

@ -30,10 +30,10 @@ cache_preheating_job:
class: "Cache::PreheatingJob"
queue: default
tracks_cleanup_job:
cron: "0 2 * * 0" # every Sunday at 02:00
class: "Tracks::CleanupJob"
queue: tracks
# tracks_cleanup_job:
# cron: "0 2 * * 0" # every Sunday at 02:00
# class: "Tracks::CleanupJob"
# queue: tracks
place_name_fetching_job:
cron: "30 0 * * *" # every day at 00:30

View file

@ -6,6 +6,7 @@
- imports
- exports
- stats
- trips
- tracks
- reverse_geocoding
- visit_suggesting

View file

@ -0,0 +1,10 @@
class AddIndexOnPlacesGeodataOsmId < ActiveRecord::Migration[8.0]
disable_ddl_transaction!
def change
add_index :places, "(geodata->'properties'->>'osm_id')",
using: :btree,
name: 'index_places_on_geodata_osm_id',
algorithm: :concurrently
end
end

6
db/schema.rb generated
View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_07_03_193657) do
ActiveRecord::Schema[8.0].define(version: 2025_07_21_204404) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
enable_extension "postgis"
@ -77,6 +77,9 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_03_193657) do
t.index ["name"], name: "index_countries_on_name"
end
create_table "data_migrations", primary_key: "version", id: :string, force: :cascade do |t|
end
create_table "exports", force: :cascade do |t|
t.string "name", null: false
t.string "url"
@ -143,6 +146,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_03_193657) 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 "(((geodata -> 'properties'::text) ->> 'osm_id'::text))", name: "index_places_on_geodata_osm_id"
t.index ["lonlat"], name: "index_places_on_lonlat", using: :gist
end

View file

@ -151,4 +151,50 @@ RSpec.describe Tracks::CreateJob, type: :job do
expect(described_class.new.queue_name).to eq('tracks')
end
end
context 'when self-hosted' do
let(:generator_instance) { instance_double(Tracks::Generator) }
let(:notification_service) { instance_double(Notifications::Create) }
let(:error_message) { 'Something went wrong' }
before do
allow(DawarichSettings).to receive(:self_hosted?).and_return(true)
allow(Tracks::Generator).to receive(:new).and_return(generator_instance)
allow(generator_instance).to receive(:call).and_raise(StandardError, error_message)
allow(Notifications::Create).to receive(:new).and_return(notification_service)
allow(notification_service).to receive(:call)
end
it 'creates a failure notification when self-hosted' do
described_class.new.perform(user.id)
expect(Notifications::Create).to have_received(:new).with(
user: user,
kind: :error,
title: 'Track Generation Failed',
content: "Failed to generate tracks from your location data: #{error_message}"
)
expect(notification_service).to have_received(:call)
end
end
context 'when not self-hosted' do
let(:generator_instance) { instance_double(Tracks::Generator) }
let(:notification_service) { instance_double(Notifications::Create) }
let(:error_message) { 'Something went wrong' }
before do
allow(DawarichSettings).to receive(:self_hosted?).and_return(false)
allow(Tracks::Generator).to receive(:new).and_return(generator_instance)
allow(generator_instance).to receive(:call).and_raise(StandardError, error_message)
allow(Notifications::Create).to receive(:new).and_return(notification_service)
allow(notification_service).to receive(:call)
end
it 'does not create a failure notification' do
described_class.new.perform(user.id)
expect(notification_service).not_to have_received(:call)
end
end
end

View file

@ -30,7 +30,7 @@ RSpec.describe Point, type: :model do
end
end
describe '#recalculate_track' do
xdescribe '#recalculate_track' do
let(:point) { create(:point, track: track) }
let(:track) { create(:track) }
@ -121,7 +121,7 @@ RSpec.describe Point, type: :model do
end
end
describe '#trigger_incremental_track_generation' do
xdescribe '#trigger_incremental_track_generation' do
let(:point) do
create(:point, track: track, import_id: nil, timestamp: 1.hour.ago.to_i, reverse_geocoded_at: 1.hour.ago)
end

View file

@ -0,0 +1,130 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe StatsQuery do
describe '#points_stats' do
subject(:points_stats) { described_class.new(user).points_stats }
let(:user) { create(:user) }
let!(:import) { create(:import, user: user) }
context 'when user has no points' do
it 'returns zero counts for all statistics' do
expect(points_stats).to eq({
total: 0,
geocoded: 0,
without_data: 0
})
end
end
context 'when user has points' do
let!(:geocoded_point_with_data) do
create(:point,
user: user,
import: import,
reverse_geocoded_at: Time.current,
geodata: { 'address' => '123 Main St' })
end
let!(:geocoded_point_without_data) do
create(:point,
user: user,
import: import,
reverse_geocoded_at: Time.current,
geodata: {})
end
let!(:non_geocoded_point) do
create(:point,
user: user,
import: import,
reverse_geocoded_at: nil,
geodata: { 'some' => 'data' })
end
it 'returns correct counts for all statistics' do
expect(points_stats).to eq({
total: 3,
geocoded: 2,
without_data: 1
})
end
context 'when another user has points' do
let(:other_user) { create(:user) }
let!(:other_import) { create(:import, user: other_user) }
let!(:other_point) do
create(:point,
user: other_user,
import: other_import,
reverse_geocoded_at: Time.current,
geodata: { 'address' => 'Other Address' })
end
it 'only counts points for the specified user' do
expect(points_stats).to eq({
total: 3,
geocoded: 2,
without_data: 1
})
end
end
end
context 'when all points are geocoded with data' do
before do
create_list(:point, 5,
user: user,
import: import,
reverse_geocoded_at: Time.current,
geodata: { 'address' => 'Some Address' })
end
it 'returns correct statistics' do
expect(points_stats).to eq({
total: 5,
geocoded: 5,
without_data: 0
})
end
end
context 'when all points are without geodata' do
before do
create_list(:point, 3,
user: user,
import: import,
reverse_geocoded_at: Time.current,
geodata: {})
end
it 'returns correct statistics' do
expect(points_stats).to eq({
total: 3,
geocoded: 3,
without_data: 3
})
end
end
context 'when all points are not geocoded' do
before do
create_list(:point, 4,
user: user,
import: import,
reverse_geocoded_at: nil,
geodata: { 'some' => 'data' })
end
it 'returns correct statistics' do
expect(points_stats).to eq({
total: 4,
geocoded: 0,
without_data: 0
})
end
end
end
end

View file

@ -28,6 +28,11 @@ RSpec.describe PointsLimitExceeded do
end
it { is_expected.to be true }
it 'caches the result' do
expect(user.tracked_points).to receive(:count).once
2.times { described_class.new(user).call }
end
end
context 'when user points count exceeds the limit' do