mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Move point creation to a background job, add months navigation to the sidebar
This commit is contained in:
parent
b6769676c3
commit
5544bcd5ff
22 changed files with 168 additions and 56 deletions
|
|
@ -1 +1,4 @@
|
|||
require: rubocop-rails
|
||||
|
||||
Style/Documentation:
|
||||
Enabled: false
|
||||
|
|
|
|||
7
Gemfile
7
Gemfile
|
|
@ -15,10 +15,11 @@ gem 'stimulus-rails'
|
|||
gem 'tailwindcss-rails'
|
||||
gem 'turbo-rails'
|
||||
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
|
||||
gem "importmap-rails"
|
||||
gem "chartkick"
|
||||
gem 'importmap-rails'
|
||||
gem 'chartkick'
|
||||
gem 'geocoder'
|
||||
gem 'sidekiq'
|
||||
gem 'sidekiq-cron'
|
||||
|
||||
|
||||
group :development, :test do
|
||||
|
|
@ -42,4 +43,4 @@ group :development do
|
|||
end
|
||||
|
||||
# Use Redis for Action Cable
|
||||
gem "redis"
|
||||
gem 'redis'
|
||||
|
|
|
|||
11
Gemfile.lock
11
Gemfile.lock
|
|
@ -106,6 +106,8 @@ GEM
|
|||
railties (>= 6.1)
|
||||
drb (2.2.1)
|
||||
erubi (1.12.0)
|
||||
et-orbi (1.2.11)
|
||||
tzinfo
|
||||
factory_bot (6.4.6)
|
||||
activesupport (>= 5.0.0)
|
||||
factory_bot_rails (6.4.3)
|
||||
|
|
@ -113,6 +115,9 @@ GEM
|
|||
railties (>= 5.0.0)
|
||||
ffaker (2.23.0)
|
||||
foreman (0.87.2)
|
||||
fugit (1.10.1)
|
||||
et-orbi (~> 1, >= 1.2.7)
|
||||
raabro (~> 1.4)
|
||||
geocoder (1.8.2)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
|
|
@ -178,6 +183,7 @@ GEM
|
|||
nio4r (~> 2.0)
|
||||
pundit (2.3.1)
|
||||
activesupport (>= 3.0.0)
|
||||
raabro (1.4.0)
|
||||
racc (1.7.3)
|
||||
rack (3.0.10)
|
||||
rack-session (2.0.0)
|
||||
|
|
@ -274,6 +280,10 @@ GEM
|
|||
connection_pool (>= 2.3.0)
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.19.0)
|
||||
sidekiq-cron (1.12.0)
|
||||
fugit (~> 1.8)
|
||||
globalid (>= 1.0.1)
|
||||
sidekiq (>= 6)
|
||||
simplecov (0.22.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
|
|
@ -343,6 +353,7 @@ DEPENDENCIES
|
|||
rubocop-rails
|
||||
shoulda-matchers
|
||||
sidekiq
|
||||
sidekiq-cron
|
||||
simplecov
|
||||
sprockets-rails
|
||||
stimulus-rails
|
||||
|
|
|
|||
|
|
@ -1,3 +1,3 @@
|
|||
web: bin/rails server -p 3000 -b 0.0.0.0
|
||||
css: bin/rails tailwindcss:watch
|
||||
worker: bundle exec sidekiq
|
||||
worker: bundle exec sidekiq -C config/sidekiq.yml
|
||||
|
|
|
|||
|
|
@ -2,15 +2,9 @@ class Api::V1::PointsController < ApplicationController
|
|||
skip_forgery_protection
|
||||
|
||||
def create
|
||||
parsed_params = OwnTracks::Params.new(point_params).call
|
||||
PointCreatingJob.perform_later(point_params)
|
||||
|
||||
@point = Point.create(parsed_params)
|
||||
|
||||
if @point.valid?
|
||||
render json: @point, status: :ok
|
||||
else
|
||||
render json: @point.errors, status: :unprocessable_entity
|
||||
end
|
||||
render json: {}, status: :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ class PointsController < ApplicationController
|
|||
@distance = distance
|
||||
@start_at = Time.zone.at(start_at)
|
||||
@end_at = Time.zone.at(end_at)
|
||||
@years = (@start_at.year..@end_at.year).to_a
|
||||
end
|
||||
|
||||
private
|
||||
|
|
@ -18,13 +19,13 @@ class PointsController < ApplicationController
|
|||
def start_at
|
||||
return 1.month.ago.beginning_of_day.to_i if params[:start_at].nil?
|
||||
|
||||
params[:start_at].to_datetime.to_i
|
||||
Time.parse(params[:start_at]).to_i
|
||||
end
|
||||
|
||||
def end_at
|
||||
return Time.zone.today.end_of_day.to_i if params[:end_at].nil?
|
||||
|
||||
params[:end_at].to_datetime.to_i
|
||||
Time.parse(params[:end_at]).to_i
|
||||
end
|
||||
|
||||
def distance
|
||||
|
|
|
|||
|
|
@ -17,10 +17,18 @@ module ApplicationHelper
|
|||
end
|
||||
|
||||
def year_timespan(year)
|
||||
start_at = DateTime.new(year).beginning_of_year.to_time.strftime('%Y-%m-%dT%H:%M')
|
||||
end_at = DateTime.new(year).end_of_year.to_time.strftime('%Y-%m-%dT%H:%M')
|
||||
start_at = Time.utc(year).in_time_zone('Europe/Berlin').beginning_of_year.strftime('%Y-%m-%dT%H:%M')
|
||||
end_at = Time.utc(year).in_time_zone('Europe/Berlin').end_of_year.strftime('%Y-%m-%dT%H:%M')
|
||||
|
||||
{ start_at: start_at, end_at: end_at }
|
||||
{ start_at:, end_at: }
|
||||
end
|
||||
|
||||
def timespan(month, year)
|
||||
month = DateTime.new(year, month).in_time_zone(Time.zone)
|
||||
start_at = month.beginning_of_month.to_time.strftime('%Y-%m-%dT%H:%M')
|
||||
end_at = month.end_of_month.to_time.strftime('%Y-%m-%dT%H:%M')
|
||||
|
||||
{ start_at:, end_at: }
|
||||
end
|
||||
|
||||
def header_colors
|
||||
|
|
@ -38,4 +46,14 @@ module ApplicationHelper
|
|||
def year_distance_stat_in_km(year)
|
||||
Stat.year_distance(year).sum { _1[1] }
|
||||
end
|
||||
|
||||
def is_past?(year, month)
|
||||
DateTime.new(year, month).past?
|
||||
end
|
||||
|
||||
def points_exist?(year, month)
|
||||
Point.where(
|
||||
timestamp: DateTime.new(year, month).beginning_of_month..DateTime.new(year, month).end_of_month
|
||||
).exists?
|
||||
end
|
||||
end
|
||||
|
|
|
|||
9
app/jobs/point_creating_job.rb
Normal file
9
app/jobs/point_creating_job.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
class PointCreatingJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(point_params)
|
||||
parsed_params = OwnTracks::Params.new(point_params).call
|
||||
|
||||
point = Point.create(parsed_params)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class StatCreatingJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(user_id)
|
||||
CreateStats.new(user_id).call
|
||||
def perform(user_ids = nil)
|
||||
CreateStats.new(user_ids).call
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -51,4 +51,10 @@ class Stat < ApplicationRecord
|
|||
|
||||
{ countries: data.count, cities: data.sum { |country| country[:cities].count } }
|
||||
end
|
||||
|
||||
def self.years
|
||||
starting_year = pluck(:year).uniq.min || Time.current.year
|
||||
|
||||
(starting_year..Time.current.year).to_a.reverse
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,8 +37,10 @@ class CountriesAndCities
|
|||
end
|
||||
end
|
||||
|
||||
|
||||
def filter_cities(mapped_with_cities)
|
||||
# In future, we would want to remove cities where user spent less than
|
||||
# 1 hour per day
|
||||
|
||||
# Remove cities with less than MINIMUM_POINTS_IN_CITY
|
||||
mapped_with_cities.transform_values do |cities|
|
||||
cities.reject { |_, data| data[:points] < MINIMUM_POINTS_IN_CITY }
|
||||
|
|
@ -48,8 +50,8 @@ class CountriesAndCities
|
|||
def normalize_result(hash)
|
||||
hash.map do |country, cities|
|
||||
{
|
||||
country: country,
|
||||
cities: cities.map { |city, data| { city: city, points: data[:points], timestamp: data[:timestamp] } }
|
||||
country:,
|
||||
cities: cities.map { |city, data| { city:, points: data[:points], timestamp: data[:timestamp] } }
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class CreateStats
|
||||
attr_reader :years, :months, :user
|
||||
attr_reader :years, :months, :users
|
||||
|
||||
def initialize(user_id)
|
||||
@user = User.find(user_id)
|
||||
def initialize(user_ids)
|
||||
@users = User.where(id: user_ids)
|
||||
@years = (1970..Time.current.year).to_a
|
||||
@months = (1..12).to_a
|
||||
end
|
||||
|
||||
def call
|
||||
years.flat_map do |year|
|
||||
months.map do |month|
|
||||
users.each do |user|
|
||||
years.each do |year|
|
||||
months.each do |month|
|
||||
beginning_of_month_timestamp = DateTime.new(year, month).beginning_of_month.to_i
|
||||
end_of_month_timestamp = DateTime.new(year, month).end_of_month.to_i
|
||||
|
||||
|
|
@ -26,7 +27,8 @@ class CreateStats
|
|||
|
||||
stat
|
||||
end
|
||||
end.compact
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.7.2/dist/full.css" rel="stylesheet" type="text/css">
|
||||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.9.0/dist/full.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
|
||||
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<div class='w-3/4 mt-10'>
|
||||
<div class='w-4/5 mt-10'>
|
||||
<div class="flex flex-col space-y-4 mb-4 w-full">
|
||||
<%= form_with url: points_path, method: :get do |f| %>
|
||||
<div class="flex flex-col md:flex-row md:space-x-4 md:items-end">
|
||||
|
|
@ -32,7 +32,7 @@
|
|||
</div>
|
||||
|
||||
</div>
|
||||
<div class='w-1/4 mt-10'>
|
||||
<div class='w-1/5 mt-10'>
|
||||
<%= render 'shared/right_sidebar' %>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,36 @@
|
|||
<%= "#{@distance} km" if @distance %>
|
||||
|
||||
<div id='years-nav'>
|
||||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn m-1">Select year</div>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<% Stat.years.each do |year| %>
|
||||
<li><%= link_to year, points_url(year_timespan(year).merge(year: year)) %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<% @years.each do |year| %>
|
||||
<h3 class='text-xl'>
|
||||
<%= year %>
|
||||
</h3>
|
||||
|
||||
<div class='grid grid-cols-3 gap-3'>
|
||||
<% (1..12).to_a.each_slice(3) do |months| %>
|
||||
<% months.each do |month_number| %>
|
||||
<% if is_past?(year, month_number) && points_exist?(year, month_number) %>
|
||||
<%= link_to Date::ABBR_MONTHNAMES[month_number], points_url(timespan(month_number, year)), class: 'btn btn-default' %>
|
||||
<% else %>
|
||||
<div class='btn btn-disabled'><%= Date::ABBR_MONTHNAMES[month_number] %></div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if REVERSE_GEOCODING_ENABLED && @countries_and_cities&.any? %>
|
||||
<hr class='my-5'>
|
||||
<% @countries_and_cities.each do |country| %>
|
||||
<h2 class="text-lg font-semibold mt-5">
|
||||
<%= country[:country] %> (<%= country[:cities].count %> cities)
|
||||
|
|
|
|||
|
|
@ -48,10 +48,16 @@
|
|||
<%= link_to year, "/stats/#{year}", class: 'underline hover:no-underline' %>
|
||||
<%= link_to '[Map]', points_url(year_timespan(year)), class: 'underline hover:no-underline' %>
|
||||
</h2>
|
||||
<p><%= number_with_delimiter year_distance_stat_in_km(year) %>km</p>
|
||||
<p>
|
||||
<% cache [current_user, 'year_distance_stat_in_km', year], skip_digest: true do %>
|
||||
<%= number_with_delimiter year_distance_stat_in_km(year) %>km
|
||||
<% end %>
|
||||
</p>
|
||||
<% if REVERSE_GEOCODING_ENABLED %>
|
||||
<div class="card-actions justify-end">
|
||||
<% cache [current_user, 'countries_and_cities_stat', year], skip_digest: true do %>
|
||||
<%= countries_and_cities_stat(year) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= column_chart(
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
MINIMUM_POINTS_IN_CITY = ENV.fetch('MINIMUM_POINTS_IN_CITY', 5).to_i
|
||||
MAP_CENTER = ENV.fetch('MAP_CENTER', '[55.7522, 37.6156]')
|
||||
REVERSE_GEOCODING_ENABLED = ENV.fetch('REVERSE_GEOCODING_ENABLED', 'true') == 'true'
|
||||
|
|
|
|||
6
config/schedule.yml
Normal file
6
config/schedule.yml
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
# config/schedule.yml
|
||||
|
||||
stat_creating_job:
|
||||
cron: "0 */6 * * *"
|
||||
class: "StatCreatingJob"
|
||||
queue: default
|
||||
|
|
@ -1,5 +1,16 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe ImportJob, type: :job do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
describe '#perform' do
|
||||
subject(:perform) { described_class.new.perform(user.id, import.id) }
|
||||
|
||||
let(:file_path) { 'spec/fixtures/owntracks_export.json' }
|
||||
let(:file) { fixture_file_upload(file_path) }
|
||||
let(:user) { create(:user) }
|
||||
let(:import) { create(:import, user: user, file: file, name: File.basename(file.path)) }
|
||||
|
||||
it 'creates points' do
|
||||
expect { perform }.to change { Point.count }.by(8)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
15
spec/jobs/point_creating_job_spec.rb
Normal file
15
spec/jobs/point_creating_job_spec.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe PointCreatingJob, type: :job do
|
||||
describe '#perform' do
|
||||
subject(:perform) { described_class.new.perform(point_params) }
|
||||
|
||||
let(:point_params) do
|
||||
{ lat: 1.0, lon: 1.0, tid: 'test', tst: Time.now.to_i, topic: 'iPhone 12 pro' }
|
||||
end
|
||||
|
||||
it 'creates a point' do
|
||||
expect { perform }.to change { Point.count }.by(1)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -12,18 +12,11 @@ RSpec.describe "Api::V1::Points", type: :request do
|
|||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with invalid params' do
|
||||
let(:params) do
|
||||
{ lat: 1.0, lon: 1.0, tid: 'test', tst: Time.now.to_i }
|
||||
end
|
||||
|
||||
it "returns http unprocessable_entity" do
|
||||
it 'enqueues a job' do
|
||||
expect {
|
||||
post api_v1_points_path, params: params
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
expect(response.body).to eq("{\"topic\":[\"can't be blank\"]}")
|
||||
}.to have_enqueued_job(PointCreatingJob)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe OwnTracks::ExportParser do
|
||||
describe '#call' do
|
||||
subject(:parser) { described_class.new(import.id).call }
|
||||
subject(:parser) { described_class.new(import).call }
|
||||
|
||||
let(:file_path) { 'spec/fixtures/owntracks_export.json' }
|
||||
let(:file) { fixture_file_upload(file_path) }
|
||||
|
|
|
|||
Loading…
Reference in a new issue