Implement bulk points deletion

This commit is contained in:
Eugene Burmakin 2024-05-23 20:12:23 +02:00
parent 22c8a210b1
commit 814095a4a2
39 changed files with 610 additions and 118 deletions

View file

@ -1 +1 @@
0.2.5
0.3.0

View file

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

View file

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

View file

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

View file

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

View file

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

View 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

View file

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

View file

@ -87,4 +87,8 @@ module ApplicationHelper
"(#{points_pluralized})"
end
def active_class?(link_path)
'btn-active' if current_page?(link_path)
end
end

View file

@ -1,2 +0,0 @@
module ExportHelper
end

View file

@ -1,2 +0,0 @@
module HomeHelper
end

View file

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

View 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
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View 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 %>

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

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

View file

@ -1,39 +1,63 @@
<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">
<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>
<% 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">
<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>
<% 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>
<% end %>
<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>

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View 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"
}
}]
}

View file

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

View file

@ -2,31 +2,58 @@
require 'rails_helper'
RSpec.describe 'Points', 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
RSpec.describe '/points', type: :request do
describe 'GET /index' do
context 'when user signed in' 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
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)
end
end
context 'when user not signed in' do
it 'returns http success' do
get points_path
expect(response).to have_http_status(302)
expect(response).to be_successful
end
end
end
describe 'DELETE /bulk_destroy' do
let(:point1) { create(:point) }
let(:point2) { create(:point) }
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

View file

@ -172,7 +172,7 @@ paths:
lat: 52.502397
lon: 13.356718
tid: Swagger
tst: 1716037633
tst: 1716487940
servers:
- url: http://{defaultHost}
variables: