mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Implement bulk points deletion
This commit is contained in:
parent
22c8a210b1
commit
814095a4a2
39 changed files with 610 additions and 118 deletions
|
|
@ -1 +1 @@
|
|||
0.2.5
|
||||
0.3.0
|
||||
|
|
|
|||
20
CHANGELOG.md
20
CHANGELOG.md
|
|
@ -5,12 +5,32 @@ 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.3.0] — 2024-05-23
|
||||
|
||||
### Added
|
||||
|
||||
- Add Points page to display all the points as a table with pagination to allow users to delete points
|
||||
- Sidekiq web interface to monitor background jobs is now available at `/sidekiq`
|
||||
- Now you can choose a date range of points to be exported
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stop selecting `raw_data` column during requests to `imports` and `points` tables to improve performance.
|
||||
|
||||
### Changed
|
||||
|
||||
- Rename PointsController to MapController along with all the views and routes
|
||||
|
||||
---
|
||||
|
||||
## [0.2.5] — 2024-05-21
|
||||
|
||||
### Fixed
|
||||
|
||||
- Stop ignoring `raw_data` column during requests to `imports` and `points` tables. This was preventing points from being created.
|
||||
|
||||
---
|
||||
|
||||
## [0.2.4] — 2024-05-19
|
||||
|
||||
### Added
|
||||
|
|
|
|||
3
Gemfile
3
Gemfile
|
|
@ -9,6 +9,7 @@ gem 'chartkick'
|
|||
gem 'devise'
|
||||
gem 'geocoder'
|
||||
gem 'importmap-rails'
|
||||
gem 'oj'
|
||||
gem 'pg'
|
||||
gem 'puma'
|
||||
gem 'pundit'
|
||||
|
|
@ -23,6 +24,7 @@ gem 'stimulus-rails'
|
|||
gem 'tailwindcss-rails'
|
||||
gem 'turbo-rails'
|
||||
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
|
||||
gem 'will_paginate', '~> 4.0'
|
||||
|
||||
group :development, :test do
|
||||
gem 'debug', platforms: %i[mri mingw x64_mingw]
|
||||
|
|
@ -36,6 +38,7 @@ group :development, :test do
|
|||
end
|
||||
|
||||
group :test do
|
||||
gem 'fakeredis'
|
||||
gem 'shoulda-matchers'
|
||||
gem 'simplecov'
|
||||
gem 'super_diff'
|
||||
|
|
|
|||
|
|
@ -123,6 +123,7 @@ GEM
|
|||
factory_bot_rails (6.4.3)
|
||||
factory_bot (~> 6.4)
|
||||
railties (>= 5.0.0)
|
||||
fakeredis (0.1.4)
|
||||
ffaker (2.23.0)
|
||||
foreman (0.88.1)
|
||||
fugit (1.10.1)
|
||||
|
|
@ -184,6 +185,8 @@ GEM
|
|||
racc (~> 1.4)
|
||||
nokogiri (1.16.5-x86_64-linux)
|
||||
racc (~> 1.4)
|
||||
oj (3.16.3)
|
||||
bigdecimal (>= 3.0)
|
||||
optimist (3.1.0)
|
||||
orm_adapter (0.5.0)
|
||||
parallel (1.24.0)
|
||||
|
|
@ -374,6 +377,7 @@ GEM
|
|||
websocket-driver (0.7.6)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
will_paginate (4.0.0)
|
||||
zeitwerk (2.6.14)
|
||||
|
||||
PLATFORMS
|
||||
|
|
@ -391,10 +395,12 @@ DEPENDENCIES
|
|||
devise
|
||||
dotenv-rails
|
||||
factory_bot_rails
|
||||
fakeredis
|
||||
ffaker
|
||||
foreman
|
||||
geocoder
|
||||
importmap-rails
|
||||
oj
|
||||
pg
|
||||
pry-byebug
|
||||
pry-rails
|
||||
|
|
@ -419,6 +425,7 @@ DEPENDENCIES
|
|||
turbo-rails
|
||||
tzinfo-data
|
||||
webmock
|
||||
will_paginate (~> 4.0)
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.2.3p157
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -4,21 +4,48 @@ class ExportController < ApplicationController
|
|||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@export = current_user.export_data
|
||||
@start_at = Time.zone.at(start_at)
|
||||
@end_at = Time.zone.at(end_at)
|
||||
end
|
||||
|
||||
def download
|
||||
export = current_user.export_data
|
||||
export = current_user.export_data(start_at:, end_at:)
|
||||
|
||||
send_data export, filename:
|
||||
send_data export, filename:, type: 'applocation/json', disposition: 'attachment'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def filename
|
||||
first_point_datetime = Time.zone.at(current_user.points.first.timestamp).to_s
|
||||
last_point_datetime = Time.zone.at(current_user.points.last.timestamp).to_s
|
||||
first_point_datetime = Time.zone.at(start_at).to_s
|
||||
last_point_datetime = Time.zone.at(end_at).to_s
|
||||
|
||||
"dawarich-export-#{first_point_datetime}-#{last_point_datetime}.json".gsub(' ', '_')
|
||||
end
|
||||
|
||||
def start_at
|
||||
first_point_timestamp = Point.order(timestamp: :asc)&.first&.timestamp
|
||||
|
||||
@start_at ||=
|
||||
if params[:start_at].nil? && first_point_timestamp.present?
|
||||
first_point_timestamp
|
||||
elsif params[:start_at].nil?
|
||||
1.month.ago.to_i
|
||||
else
|
||||
Time.zone.parse(params[:start_at]).to_i
|
||||
end
|
||||
end
|
||||
|
||||
def end_at
|
||||
last_point_timestamp = Point.order(timestamp: :desc)&.last&.timestamp
|
||||
|
||||
@end_at ||=
|
||||
if params[:end_at].nil? && last_point_timestamp.present?
|
||||
last_point_timestamp
|
||||
elsif params[:end_at].nil?
|
||||
Time.zone.now.to_i
|
||||
else
|
||||
Time.zone.parse(params[:end_at]).to_i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class HomeController < ApplicationController
|
||||
def index
|
||||
if current_user
|
||||
redirect_to points_url
|
||||
end
|
||||
redirect_to map_url if current_user
|
||||
|
||||
@points = current_user.points if current_user
|
||||
@points = current_user.points.without_raw_data if current_user
|
||||
end
|
||||
end
|
||||
|
|
|
|||
41
app/controllers/map_controller.rb
Normal file
41
app/controllers/map_controller.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
# frozen_string_literal: true
|
||||
class MapController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@points = Point.without_raw_data.where('timestamp >= ? AND timestamp <= ?', start_at, end_at).order(timestamp: :asc)
|
||||
|
||||
@countries_and_cities = CountriesAndCities.new(@points).call
|
||||
@coordinates =
|
||||
@points.pluck(:latitude, :longitude, :battery, :altitude, :timestamp, :velocity, :id)
|
||||
.map { [_1.to_f, _2.to_f, _3.to_s, _4.to_s, _5.to_s, _6.to_s, _7] }
|
||||
@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
|
||||
|
||||
def start_at
|
||||
return 1.month.ago.beginning_of_day.to_i if params[:start_at].nil?
|
||||
|
||||
Time.zone.parse(params[:start_at]).to_i
|
||||
end
|
||||
|
||||
def end_at
|
||||
return Time.zone.today.end_of_day.to_i if params[:end_at].nil?
|
||||
|
||||
Time.zone.parse(params[:end_at]).to_i
|
||||
end
|
||||
|
||||
def distance
|
||||
@distance ||= 0
|
||||
|
||||
@coordinates.each_cons(2) do
|
||||
@distance += Geocoder::Calculations.distance_between([_1[0], _1[1]], [_2[0], _2[1]], units: :km)
|
||||
end
|
||||
|
||||
@distance.round(1)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,22 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class PointsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@points = Point.where('timestamp >= ? AND timestamp <= ?', start_at, end_at).order(timestamp: :asc)
|
||||
@points =
|
||||
Point
|
||||
.without_raw_data
|
||||
.where('timestamp >= ? AND timestamp <= ?', start_at, end_at)
|
||||
.order(timestamp: :asc)
|
||||
.paginate(page: params[:page], per_page: 50)
|
||||
|
||||
@countries_and_cities = CountriesAndCities.new(@points).call
|
||||
@coordinates =
|
||||
@points.pluck(:latitude, :longitude, :battery, :altitude, :timestamp, :velocity, :id)
|
||||
.map { [_1.to_f, _2.to_f, _3.to_s, _4.to_s, _5.to_s, _6.to_s, _7] }
|
||||
@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
|
||||
|
||||
def bulk_destroy
|
||||
Point.where(id: params[:point_ids].compact).destroy_all
|
||||
|
||||
redirect_to points_url, notice: "Points were successfully destroyed.", status: :see_other
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def point_params
|
||||
params.fetch(:point, {})
|
||||
end
|
||||
|
||||
def start_at
|
||||
return 1.month.ago.beginning_of_day.to_i if params[:start_at].nil?
|
||||
|
||||
|
|
@ -28,14 +38,4 @@ class PointsController < ApplicationController
|
|||
|
||||
Time.zone.parse(params[:end_at]).to_i
|
||||
end
|
||||
|
||||
def distance
|
||||
@distance ||= 0
|
||||
|
||||
@coordinates.each_cons(2) do
|
||||
@distance += Geocoder::Calculations.distance_between([_1[0], _1[1]], [_2[0], _2[1]], units: :km)
|
||||
end
|
||||
|
||||
@distance.round(1)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -87,4 +87,8 @@ module ApplicationHelper
|
|||
|
||||
"(#{points_pluralized})"
|
||||
end
|
||||
|
||||
def active_class?(link_path)
|
||||
'btn-active' if current_page?(link_path)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
module ExportHelper
|
||||
end
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
module HomeHelper
|
||||
end
|
||||
|
|
@ -4,7 +4,7 @@ module PointsHelper
|
|||
def link_to_date(timestamp)
|
||||
datetime = Time.zone.at(timestamp)
|
||||
|
||||
link_to points_path(start_at: datetime.beginning_of_day, end_at: datetime.end_of_day), \
|
||||
link_to map_path(start_at: datetime.beginning_of_day, end_at: datetime.end_of_day), \
|
||||
class: 'underline hover:no-underline' do
|
||||
datetime.strftime('%d.%m.%Y')
|
||||
end
|
||||
|
|
|
|||
29
app/javascript/controllers/checkbox_select_all_controller.js
Normal file
29
app/javascript/controllers/checkbox_select_all_controller.js
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
|
||||
// Connects to data-controller="checkbox-select-all"
|
||||
export default class extends Controller {
|
||||
static targets = ["parent", "child"]
|
||||
|
||||
connect() {
|
||||
this.parentTarget.checked = false
|
||||
this.childTargets.map(x => x.checked = false)
|
||||
}
|
||||
|
||||
toggleChildren() {
|
||||
if (this.parentTarget.checked) {
|
||||
this.childTargets.map(x => x.checked = true)
|
||||
console.log('toggleChildrenChecked')
|
||||
} else {
|
||||
this.childTargets.map(x => x.checked = false)
|
||||
console.log('toggleChildrenUNChecked')
|
||||
}
|
||||
}
|
||||
|
||||
toggleParent() {
|
||||
if (this.childTargets.map(x => x.checked).includes(false)) {
|
||||
this.parentTarget.checked = false
|
||||
} else {
|
||||
this.parentTarget.checked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Point < ApplicationRecord
|
||||
# self.ignored_columns = %w[raw_data]
|
||||
|
||||
|
|
@ -15,6 +17,14 @@ class Point < ApplicationRecord
|
|||
|
||||
after_create :async_reverse_geocode
|
||||
|
||||
def self.without_raw_data
|
||||
select(column_names - ['raw_data'])
|
||||
end
|
||||
|
||||
def recorded_at
|
||||
Time.zone.at(timestamp)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def async_reverse_geocode
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ class Stat < ApplicationRecord
|
|||
end_of_day = day.end_of_day.to_i
|
||||
|
||||
# We have to filter by user as well
|
||||
points = Point.where(timestamp: beginning_of_day..end_of_day)
|
||||
points = Point.without_raw_data.where(timestamp: beginning_of_day..end_of_day)
|
||||
|
||||
data = { day: index, distance: 0 }
|
||||
|
||||
|
|
|
|||
|
|
@ -12,8 +12,10 @@ class User < ApplicationRecord
|
|||
|
||||
after_create :create_api_key
|
||||
|
||||
def export_data
|
||||
::ExportSerializer.new(points, email).call
|
||||
def export_data(start_at: nil, end_at: nil)
|
||||
geopoints = time_framed_points(start_at, end_at)
|
||||
|
||||
::ExportSerializer.new(geopoints, email).call
|
||||
end
|
||||
|
||||
def countries_visited
|
||||
|
|
@ -46,13 +48,26 @@ class User < ApplicationRecord
|
|||
end
|
||||
|
||||
def total_reverse_geocoded
|
||||
points.where.not(country: nil, city: nil).count
|
||||
points.select(:id).where.not(country: nil, city: nil).count
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def create_api_key
|
||||
self.api_key = SecureRandom.hex(16)
|
||||
|
||||
save
|
||||
end
|
||||
|
||||
def time_framed_points(start_at, end_at)
|
||||
return points.without_raw_data if start_at.nil? && end_at.nil?
|
||||
|
||||
if start_at && end_at
|
||||
points.without_raw_data.where('timestamp >= ? AND timestamp <= ?', start_at, end_at)
|
||||
elsif start_at
|
||||
points.without_raw_data.where('timestamp >= ?', start_at)
|
||||
elsif end_at
|
||||
points.without_raw_data.where('timestamp <= ?', end_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ class CreateStats
|
|||
|
||||
def points(beginning_of_month_timestamp, end_of_month_timestamp)
|
||||
Point
|
||||
.without_raw_data
|
||||
.where(timestamp: beginning_of_month_timestamp..end_of_month_timestamp)
|
||||
.order(:timestamp)
|
||||
.select(:latitude, :longitude, :timestamp, :city, :country)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,32 @@
|
|||
<div class="w-full">
|
||||
<div class='m-5'>
|
||||
<h1 class='text-3xl font-bold'>Export Data</h1>
|
||||
<%= link_to 'Download JSON', export_download_path, class: 'btn btn-primary my-5' %>
|
||||
|
||||
<div role="alert" class="alert alert-info my-5">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
|
||||
<span>Default selected timeframes are based on first and last geopoint timestamps</span>
|
||||
</div>
|
||||
|
||||
<%= form_with url: export_download_path, method: :get, data: { turbo: false } do |f| %>
|
||||
<div class="flex flex-col md:flex-row md:space-x-4 md:items-end">
|
||||
<div class="w-full md:w-1/3">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<%= f.label :start_at, class: "text-sm font-semibold" %>
|
||||
<%= f.datetime_local_field :start_at, class: "rounded-md w-full", value: @start_at %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/3">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<%= f.label :end_at, class: "text-sm font-semibold" %>
|
||||
<%= f.datetime_local_field :end_at, class: "rounded-md w-full", value: @end_at %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/3">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<%= f.submit "Download JSON", class: "px-4 py-2 bg-blue-500 text-white rounded-md" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<%= form_with(model: import, class: "contents") do |form| %>
|
||||
<%= form_with model: import, class: "contents" do |form| %>
|
||||
<% if import.errors.any? %>
|
||||
<div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
|
||||
<h2><%= pluralize(import.errors.count, "error") %> prohibited this import from being saved:</h2>
|
||||
|
|
|
|||
39
app/views/map/index.html.erb
Normal file
39
app/views/map/index.html.erb
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
<div class='w-4/5 mt-10'>
|
||||
<div class="flex flex-col space-y-4 mb-4 w-full">
|
||||
<%= form_with url: map_path, method: :get do |f| %>
|
||||
<div class="flex flex-col md:flex-row md:space-x-4 md:items-end">
|
||||
<div class="w-full md:w-1/3">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<%= f.label :start_at, class: "text-sm font-semibold" %>
|
||||
<%= f.datetime_local_field :start_at, class: "rounded-md w-full", value: @start_at %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/3">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<%= f.label :end_at, class: "text-sm font-semibold" %>
|
||||
<%= f.datetime_local_field :end_at, class: "rounded-md w-full", value: @end_at %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="w-full md:w-1/3">
|
||||
<div class="flex flex-col space-y-2">
|
||||
<%= f.submit "Search", class: "px-4 py-2 bg-blue-500 text-white rounded-md" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div
|
||||
class="w-full"
|
||||
data-controller="maps"
|
||||
data-coordinates="<%= @coordinates %>"
|
||||
data-center="<%= MAP_CENTER %>">
|
||||
<div data-maps-target="container" class="h-[25rem] w-auto min-h-screen"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class='w-1/5 mt-10'>
|
||||
<%= render 'shared/right_sidebar' %>
|
||||
</div>
|
||||
|
||||
|
||||
17
app/views/points/_form.html.erb
Normal file
17
app/views/points/_form.html.erb
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<%= form_with(model: point, class: "contents") do |form| %>
|
||||
<% if point.errors.any? %>
|
||||
<div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
|
||||
<h2><%= pluralize(point.errors.count, "error") %> prohibited this point from being saved:</h2>
|
||||
|
||||
<ul>
|
||||
<% point.errors.each do |error| %>
|
||||
<li><%= error.full_message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="inline">
|
||||
<%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
|
||||
</div>
|
||||
<% end %>
|
||||
19
app/views/points/_point.html.erb
Normal file
19
app/views/points/_point.html.erb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<tr id="<%= dom_id point %>" class='hover'>
|
||||
<td>
|
||||
<%= check_box_tag "point_ids[]",
|
||||
point.id,
|
||||
nil,
|
||||
{
|
||||
multiple: true,
|
||||
form: :bulk_destroy_form,
|
||||
data: {
|
||||
checkbox_select_all_target: 'child',
|
||||
action: 'change->checkbox-select-all#toggleParent'
|
||||
}
|
||||
}
|
||||
%>
|
||||
</td>
|
||||
<td><%= point.recorded_at %></td>
|
||||
<td><%= point.latitude %>, <%= point.longitude %></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
8
app/views/points/edit.html.erb
Normal file
8
app/views/points/edit.html.erb
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<div class="mx-auto md:w-2/3 w-full">
|
||||
<h1 class="font-bold text-4xl">Editing point</h1>
|
||||
|
||||
<%= render "form", point: @point %>
|
||||
|
||||
<%= link_to "Show this point", @point, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
<%= link_to "Back to points", points_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
</div>
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
<div class='w-4/5 mt-10'>
|
||||
<div class="flex flex-col space-y-4 mb-4 w-full">
|
||||
<% content_for :title, "Points" %>
|
||||
|
||||
<div class="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">
|
||||
<div class="w-full md:w-1/3">
|
||||
|
|
@ -22,18 +23,41 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<div
|
||||
class="w-full"
|
||||
data-controller="maps"
|
||||
data-coordinates="<%= @coordinates %>"
|
||||
data-center="<%= MAP_CENTER %>">
|
||||
<div data-maps-target="container" class="h-[25rem] w-auto min-h-screen"></div>
|
||||
</div>
|
||||
<div class='text-center my-5'>
|
||||
<%= will_paginate @points %>
|
||||
</div>
|
||||
|
||||
<div id="points" class="min-w-full">
|
||||
<div data-controller='checkbox-select-all'>
|
||||
<%= form_with url: bulk_destroy_points_path, method: :delete, id: :bulk_destroy_form do |f| %>
|
||||
<%= f.submit "Delete Selected", class: "px-4 py-2 bg-red-500 text-white rounded-md", data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?" } %>
|
||||
<table class='table'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<%= label_tag do %>
|
||||
Select all
|
||||
<%= check_box_tag 'Select all',
|
||||
id: :select_all_points,
|
||||
data: {
|
||||
checkbox_select_all_target: 'parent',
|
||||
action: 'change->checkbox-select-all#toggleChildren'
|
||||
}
|
||||
%>
|
||||
<% end %>
|
||||
</div>
|
||||
</th>
|
||||
<th>Recorded At</th>
|
||||
<th>Coordinates</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @points.each do |point| %>
|
||||
<%= render point %>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class='w-1/5 mt-10'>
|
||||
<%= render 'shared/right_sidebar' %>
|
||||
</div>
|
||||
|
||||
|
||||
|
|
|
|||
7
app/views/points/new.html.erb
Normal file
7
app/views/points/new.html.erb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<div class="mx-auto md:w-2/3 w-full">
|
||||
<h1 class="font-bold text-4xl">New point</h1>
|
||||
|
||||
<%= render "form", point: @point %>
|
||||
|
||||
<%= link_to "Back to points", points_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
</div>
|
||||
15
app/views/points/show.html.erb
Normal file
15
app/views/points/show.html.erb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<div class="mx-auto md:w-2/3 w-full flex">
|
||||
<div class="mx-auto">
|
||||
<% if notice.present? %>
|
||||
<p class="py-2 px-3 bg-green-50 mb-5 text-green-500 font-medium rounded-lg inline-block" id="notice"><%= notice %></p>
|
||||
<% end %>
|
||||
|
||||
<%= render @point %>
|
||||
|
||||
<%= link_to "Edit this point", edit_point_path(@point), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
<%= link_to "Back to points", points_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
<div class="inline-block ml-2">
|
||||
<%= button_to "Destroy this point", @point, method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -5,9 +5,10 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h8m-8 6h16" /></svg>
|
||||
</label>
|
||||
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<li><%= link_to 'Points', points_url %></li>
|
||||
<li><%= link_to 'Stats', stats_url %></li>
|
||||
<li><%= link_to 'Imports', imports_url %></li>
|
||||
<li><%= link_to 'Map', map_url, class: "#{active_class?(map_url)}" %></li>
|
||||
<li><%= link_to 'Points', points_url, class: "#{active_class?(points_url)}" %></li>
|
||||
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
|
||||
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
|
||||
</ul>
|
||||
</div>
|
||||
<%= link_to 'DaWarIch', root_path, class: 'btn btn-ghost normal-case text-xl'%>
|
||||
|
|
@ -36,9 +37,10 @@
|
|||
</div>
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
<li><%= link_to 'Points', points_url %></li>
|
||||
<li><%= link_to 'Stats', stats_url %></li>
|
||||
<li><%= link_to 'Imports', imports_url %></li>
|
||||
<li><%= link_to 'Map', map_url, class: "#{active_class?(map_url)}" %></li>
|
||||
<li><%= link_to 'Points', points_url, class: "#{active_class?(points_url)}" %></li>
|
||||
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
|
||||
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
<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>
|
||||
<li><%= link_to year, map_url(year_timespan(year).merge(year: year)) %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
|
|
@ -19,7 +19,7 @@
|
|||
<% (1..12).to_a.each_slice(3) do |months| %>
|
||||
<% months.each do |month_number| %>
|
||||
<% if 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' %>
|
||||
<%= link_to Date::ABBR_MONTHNAMES[month_number], map_url(timespan(month_number, year)), class: 'btn btn-default' %>
|
||||
<% else %>
|
||||
<div class='btn btn-disabled'><%= Date::ABBR_MONTHNAMES[month_number] %></div>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div id="<%= dom_id stat %>" class="card w-full bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<%= link_to points_url(month_timespan(stat)), class: "underline hover:no-underline text-#{header_colors.sample}" do %>
|
||||
<%= link_to map_url(month_timespan(stat)), class: "underline hover:no-underline text-#{header_colors.sample}" do %>
|
||||
<%= "#{Date::MONTHNAMES[stat.month]} of #{stat.year}" %>
|
||||
<% end %>
|
||||
</h2>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
<h2 class='text-3xl font-bold mt-10'>
|
||||
<%= link_to year, "/stats/#{year}", class: "underline hover:no-underline text-#{header_colors.sample}" %>
|
||||
<%= link_to '[Map]', points_url(year_timespan(year)), class: 'underline hover:no-underline' %>
|
||||
<%= link_to '[Map]', map_url(year_timespan(year)), class: 'underline hover:no-underline' %>
|
||||
</h2>
|
||||
<div class='my-10'>
|
||||
<%= column_chart(
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<div class="stat text-center">
|
||||
<div class="stat-value text-success">
|
||||
<%= number_with_delimiter current_user.points.count %>
|
||||
<%= number_with_delimiter current_user.points.without_raw_data.count(:id) %>
|
||||
</div>
|
||||
<div class="stat-title">Geopoints tracked</div>
|
||||
</div>
|
||||
|
|
@ -50,7 +50,7 @@
|
|||
<div class="stat-title">Cities visited</div>
|
||||
<dialog id="cities_visited" class="modal">
|
||||
<div class="modal-box">
|
||||
<h3 class="font-bold text-lg">Countries visited</h3>
|
||||
<h3 class="font-bold text-lg">Cities visited</h3>
|
||||
<p class="py-4">
|
||||
<% current_user.cities_visited.each do |city| %>
|
||||
<p><%= city %></p>
|
||||
|
|
@ -73,7 +73,7 @@
|
|||
<div class="card-body">
|
||||
<h2 class="card-title text-<%= header_colors.sample %>">
|
||||
<%= link_to year, "/stats/#{year}", class: 'underline hover:no-underline' %>
|
||||
<%= link_to '[Map]', points_url(year_timespan(year)), class: 'underline hover:no-underline' %>
|
||||
<%= link_to '[Map]', map_url(year_timespan(year)), class: 'underline hover:no-underline' %>
|
||||
</h2>
|
||||
<p>
|
||||
<% cache [current_user, 'year_distance_stat_in_km', year], skip_digest: true do %>
|
||||
|
|
|
|||
|
|
@ -1,13 +1,21 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'sidekiq/web'
|
||||
|
||||
Rails.application.routes.draw do
|
||||
mount Rswag::Api::Engine => '/api-docs'
|
||||
mount Rswag::Ui::Engine => '/api-docs'
|
||||
mount Sidekiq::Web => '/sidekiq'
|
||||
get 'settings/theme', to: 'settings#theme'
|
||||
get 'export', to: 'export#index'
|
||||
get 'export/download', to: 'export#download'
|
||||
|
||||
resources :imports
|
||||
resources :points, only: %i[index] do
|
||||
collection do
|
||||
delete :bulk_destroy
|
||||
end
|
||||
end
|
||||
resources :stats, only: :index do
|
||||
collection do
|
||||
post :update
|
||||
|
|
@ -20,7 +28,7 @@ Rails.application.routes.draw do
|
|||
|
||||
post 'settings/generate_api_key', to: 'devise/api_keys#create', as: :generate_api_key
|
||||
|
||||
get 'points', to: 'points#index'
|
||||
get 'map', to: 'map#index'
|
||||
|
||||
namespace :api do
|
||||
namespace :v1 do
|
||||
|
|
|
|||
|
|
@ -1,26 +1,28 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
FactoryBot.define do
|
||||
factory :point do
|
||||
battery_status { 1 }
|
||||
ping { "MyString" }
|
||||
ping { 'MyString' }
|
||||
battery { 1 }
|
||||
topic { "MyString" }
|
||||
topic { 'MyString' }
|
||||
altitude { 1 }
|
||||
longitude { "MyString" }
|
||||
velocity { "MyString" }
|
||||
longitude { 'MyString' }
|
||||
velocity { 'MyString' }
|
||||
trigger { 1 }
|
||||
bssid { "MyString" }
|
||||
ssid { "MyString" }
|
||||
bssid { 'MyString' }
|
||||
ssid { 'MyString' }
|
||||
connection { 1 }
|
||||
vertical_accuracy { 1 }
|
||||
accuracy { 1 }
|
||||
timestamp { 1 }
|
||||
latitude { "MyString" }
|
||||
latitude { 'MyString' }
|
||||
mode { 1 }
|
||||
inrids { "MyString" }
|
||||
in_regions { "MyString" }
|
||||
raw_data { "" }
|
||||
tracker_id { "MyString" }
|
||||
import_id { "" }
|
||||
inrids { 'MyString' }
|
||||
in_regions { 'MyString' }
|
||||
raw_data { '' }
|
||||
tracker_id { 'MyString' }
|
||||
import_id { '' }
|
||||
city { nil }
|
||||
country { nil }
|
||||
end
|
||||
|
|
|
|||
113
spec/fixtures/files/google/semantic_history.json
vendored
Normal file
113
spec/fixtures/files/google/semantic_history.json
vendored
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
{
|
||||
"timelineObjects": [{
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
},
|
||||
"endLocation": {
|
||||
},
|
||||
"duration": {
|
||||
"startTimestamp": "2013-12-01T07:05:14.222Z",
|
||||
"endTimestamp": "2013-12-01T07:11:13.214Z"
|
||||
},
|
||||
"confidence": "LOW",
|
||||
"activities": [{
|
||||
"activityType": "WALKING",
|
||||
"probability": 0.0
|
||||
}, {
|
||||
"activityType": "CYCLING",
|
||||
"probability": 0.0
|
||||
}, {
|
||||
"activityType": "IN_VEHICLE",
|
||||
"probability": 0.0
|
||||
}],
|
||||
"waypointPath": {
|
||||
"waypoints": [{
|
||||
"latE7": 533407440,
|
||||
"lngE7": 837026901
|
||||
}, {
|
||||
"latE7": 533410301,
|
||||
"lngE7": 837051010
|
||||
}],
|
||||
"source": "BACKFILLED",
|
||||
"distanceMeters": 209.65263509609417,
|
||||
"travelMode": "WALK",
|
||||
"confidence": 1.0
|
||||
},
|
||||
"editConfirmationStatus": "NOT_CONFIRMED"
|
||||
}
|
||||
}, {
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
},
|
||||
"endLocation": {
|
||||
},
|
||||
"duration": {
|
||||
"startTimestamp": "2013-12-01T07:11:13.214Z",
|
||||
"endTimestamp": "2013-12-01T08:25:17.226Z"
|
||||
},
|
||||
"distance": 3853,
|
||||
"confidence": "LOW",
|
||||
"activities": [{
|
||||
"activityType": "IN_VEHICLE",
|
||||
"probability": 0.0
|
||||
}, {
|
||||
"activityType": "WALKING",
|
||||
"probability": 0.0
|
||||
}, {
|
||||
"activityType": "CYCLING",
|
||||
"probability": 0.0
|
||||
}],
|
||||
"waypointPath": {
|
||||
"waypoints": [{
|
||||
"latE7": 533410301,
|
||||
"lngE7": 837051010
|
||||
}, {
|
||||
"latE7": 533519706,
|
||||
"lngE7": 837596359
|
||||
}],
|
||||
"source": "BACKFILLED",
|
||||
"distanceMeters": 4877.009216418153,
|
||||
"travelMode": "DRIVE",
|
||||
"confidence": 1.0
|
||||
},
|
||||
"editConfirmationStatus": "NOT_CONFIRMED"
|
||||
}
|
||||
}, {
|
||||
"activitySegment": {
|
||||
"startLocation": {
|
||||
},
|
||||
"endLocation": {
|
||||
},
|
||||
"duration": {
|
||||
"startTimestamp": "2013-12-01T08:25:17.226Z",
|
||||
"endTimestamp": "2013-12-01T09:11:45.637Z"
|
||||
},
|
||||
"distance": 413,
|
||||
"confidence": "LOW",
|
||||
"activities": [{
|
||||
"activityType": "WALKING",
|
||||
"probability": 0.0
|
||||
}, {
|
||||
"activityType": "CYCLING",
|
||||
"probability": 0.0
|
||||
}, {
|
||||
"activityType": "IN_VEHICLE",
|
||||
"probability": 0.0
|
||||
}],
|
||||
"waypointPath": {
|
||||
"waypoints": [{
|
||||
"latE7": 533519706,
|
||||
"lngE7": 837596359
|
||||
}, {
|
||||
"latE7": 533481369,
|
||||
"lngE7": 837608337
|
||||
}],
|
||||
"source": "BACKFILLED",
|
||||
"distanceMeters": 529.8583466261781,
|
||||
"travelMode": "WALK",
|
||||
"confidence": 1.0
|
||||
},
|
||||
"editConfirmationStatus": "NOT_CONFIRMED"
|
||||
}
|
||||
}]
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@ RSpec.describe ReverseGeocodingJob, type: :job do
|
|||
before { stub_const('REVERSE_GEOCODING_ENABLED', false) }
|
||||
|
||||
it 'does not update point' do
|
||||
expect { perform }.not_to change { point.reload.city }
|
||||
expect { perform }.not_to(change { point.reload.city })
|
||||
end
|
||||
|
||||
it 'does not call Geocoder' do
|
||||
|
|
|
|||
32
spec/requests/map_spec.rb
Normal file
32
spec/requests/map_spec.rb
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Map', type: :request do
|
||||
before do
|
||||
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
|
||||
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
|
||||
end
|
||||
|
||||
describe 'GET /index' do
|
||||
context 'when user signed in' do
|
||||
before do
|
||||
sign_in create(:user)
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
get map_path
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user not signed in' do
|
||||
it 'returns http success' do
|
||||
get map_path
|
||||
|
||||
expect(response).to have_http_status(302)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -2,31 +2,58 @@
|
|||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Points', type: :request do
|
||||
RSpec.describe '/points', type: :request do
|
||||
describe 'GET /index' do
|
||||
before do
|
||||
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
|
||||
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
|
||||
end
|
||||
|
||||
describe 'GET /index' do
|
||||
context 'when user signed in' do
|
||||
context 'when user is not logged in' do
|
||||
it 'redirects to login page' do
|
||||
get points_url
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is logged in' do
|
||||
before do
|
||||
sign_in create(:user)
|
||||
end
|
||||
|
||||
it 'returns http success' do
|
||||
get points_path
|
||||
it 'renders a successful response' do
|
||||
get points_url
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user not signed in' do
|
||||
it 'returns http success' do
|
||||
get points_path
|
||||
describe 'DELETE /bulk_destroy' do
|
||||
let(:point1) { create(:point) }
|
||||
let(:point2) { create(:point) }
|
||||
|
||||
expect(response).to have_http_status(302)
|
||||
before do
|
||||
sign_in create(:user)
|
||||
end
|
||||
|
||||
it 'destroys the selected points' do
|
||||
delete bulk_destroy_points_url, params: { point_ids: [point1.id, point2.id] }
|
||||
|
||||
expect(Point.find_by(id: point1.id)).to be_nil
|
||||
expect(Point.find_by(id: point2.id)).to be_nil
|
||||
end
|
||||
|
||||
it 'returns a 303 status code' do
|
||||
delete bulk_destroy_points_url, params: { point_ids: [point1.id, point2.id] }
|
||||
|
||||
expect(response).to have_http_status(303)
|
||||
end
|
||||
|
||||
it 'redirects to the points list' do
|
||||
delete bulk_destroy_points_url, params: { point_ids: [point1.id, point2.id] }
|
||||
|
||||
expect(response).to redirect_to(points_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -172,7 +172,7 @@ paths:
|
|||
lat: 52.502397
|
||||
lon: 13.356718
|
||||
tid: Swagger
|
||||
tst: 1716037633
|
||||
tst: 1716487940
|
||||
servers:
|
||||
- url: http://{defaultHost}
|
||||
variables:
|
||||
|
|
|
|||
Loading…
Reference in a new issue