mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Migrate from old template
This commit is contained in:
parent
f74415217c
commit
3c74bc2937
38 changed files with 847 additions and 22 deletions
|
|
@ -1,4 +1,4 @@
|
|||
DATABASE_HOST=dawarich_db
|
||||
DATABASE_HOST=localhost
|
||||
DATABASE_USERNAME=postgres
|
||||
DATABASE_PASSWORD=password
|
||||
DATABASE_NAME=dawarich_development
|
||||
|
|
|
|||
1
.env.template
Normal file
1
.env.template
Normal file
|
|
@ -0,0 +1 @@
|
|||
MAPBOX_ACCESS_TOKEN=MAPBOX_ACCESS_TOKEN
|
||||
5
Gemfile
5
Gemfile
|
|
@ -17,12 +17,17 @@ gem 'tailwindcss-rails'
|
|||
gem 'turbo-rails'
|
||||
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
|
||||
gem "importmap-rails"
|
||||
gem "mapkick-rb"
|
||||
gem 'geocoder'
|
||||
gem 'sidekiq'
|
||||
|
||||
|
||||
group :development, :test do
|
||||
gem 'debug', platforms: %i[mri mingw x64_mingw]
|
||||
gem 'factory_bot_rails'
|
||||
gem 'ffaker'
|
||||
gem 'rspec-rails'
|
||||
gem 'dotenv-rails'
|
||||
end
|
||||
|
||||
group :test do
|
||||
|
|
|
|||
15
Gemfile.lock
15
Gemfile.lock
|
|
@ -97,6 +97,10 @@ GEM
|
|||
warden (~> 1.2.3)
|
||||
diff-lcs (1.5.1)
|
||||
docile (1.4.0)
|
||||
dotenv (3.1.0)
|
||||
dotenv-rails (3.1.0)
|
||||
dotenv (= 3.1.0)
|
||||
railties (>= 6.1)
|
||||
drb (2.2.1)
|
||||
erubi (1.12.0)
|
||||
factory_bot (6.4.6)
|
||||
|
|
@ -106,6 +110,7 @@ GEM
|
|||
railties (>= 5.0.0)
|
||||
ffaker (2.23.0)
|
||||
foreman (0.87.2)
|
||||
geocoder (1.8.2)
|
||||
globalid (1.2.1)
|
||||
activesupport (>= 6.1)
|
||||
i18n (1.14.4)
|
||||
|
|
@ -128,6 +133,7 @@ GEM
|
|||
net-imap
|
||||
net-pop
|
||||
net-smtp
|
||||
mapkick-rb (0.1.5)
|
||||
marcel (1.0.4)
|
||||
mini_mime (1.1.5)
|
||||
minitest (5.22.3)
|
||||
|
|
@ -252,6 +258,11 @@ GEM
|
|||
ruby-progressbar (1.13.0)
|
||||
shoulda-matchers (6.1.0)
|
||||
activesupport (>= 5.2.0)
|
||||
sidekiq (7.2.2)
|
||||
concurrent-ruby (< 2)
|
||||
connection_pool (>= 2.3.0)
|
||||
rack (>= 2.2.4)
|
||||
redis-client (>= 0.19.0)
|
||||
simplecov (0.22.0)
|
||||
docile (~> 1.1)
|
||||
simplecov-html (~> 0.11)
|
||||
|
|
@ -305,10 +316,13 @@ DEPENDENCIES
|
|||
bootsnap
|
||||
debug
|
||||
devise
|
||||
dotenv-rails
|
||||
factory_bot_rails
|
||||
ffaker
|
||||
foreman
|
||||
geocoder
|
||||
importmap-rails
|
||||
mapkick-rb
|
||||
pg
|
||||
puma
|
||||
pundit
|
||||
|
|
@ -317,6 +331,7 @@ DEPENDENCIES
|
|||
rspec-rails
|
||||
rubocop-rails
|
||||
shoulda-matchers
|
||||
sidekiq
|
||||
simplecov
|
||||
sprockets-rails
|
||||
stimulus-rails
|
||||
|
|
|
|||
114
README.md
114
README.md
|
|
@ -1,17 +1,16 @@
|
|||
# Dawarich
|
||||
|
||||
This is a Rails 7.0.2.3 app template with test suite, user auth and development docker env.
|
||||
This is a Rails app that receives location updates from Owntracks and stores them in a database. It also provides a web interface to view the location history.
|
||||
|
||||
## How to rename the app
|
||||
## Features
|
||||
|
||||
Run
|
||||
### Google Maps Timeline import
|
||||
|
||||
```bash
|
||||
ruby rename_app.rb old_app_name new_app_name
|
||||
```
|
||||
You can import your Google Maps Timeline data into Wardu.
|
||||
|
||||
Notice, the name must be in snake_case. Default app name is `dawarich`.
|
||||
### Location history
|
||||
|
||||
You can view your location history on a map.
|
||||
|
||||
## How to start the app locally
|
||||
|
||||
|
|
@ -40,7 +39,7 @@ Dockerized with https://betterprogramming.pub/rails-6-development-with-docker-55
|
|||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"predeploy": "dokku ps:stop dawarich"
|
||||
"predeploy": "dokku ps:stop wardu"
|
||||
},
|
||||
"formation": {
|
||||
"web": {
|
||||
|
|
@ -52,3 +51,102 @@ Dockerized with https://betterprogramming.pub/rails-6-development-with-docker-55
|
|||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
{
|
||||
"cog": 271,
|
||||
"batt": 41,
|
||||
"lon": 2.29513,
|
||||
"acc": 5,
|
||||
"vel": 61,
|
||||
"vac": 21,
|
||||
"lat": 48.85833,
|
||||
"t": "u",
|
||||
"tst": 1497508651,
|
||||
"alt": 167,
|
||||
"_type": "location",
|
||||
"topic": "owntracks/jane/iphone",
|
||||
"p": 71,
|
||||
"tid": "JJ"
|
||||
}
|
||||
|
||||
{"bs"=>1, # battery status
|
||||
"p"=>102.818, # ping
|
||||
"batt"=>100, # battery
|
||||
"_type"=>"location", # type
|
||||
"tid"=>"RO", # Tracker ID used to display the initials of a user (iOS,Android/string/optional) required for http mode
|
||||
"topic"=>"owntracks/Frey/iPhone 12 Pro",
|
||||
"alt"=>36,
|
||||
"lon"=>13.504178,
|
||||
"vel"=>0, # velocity
|
||||
"t"=>"u",
|
||||
"BSSID"=>"b0:f2:8:45:94:33",
|
||||
"SSID"=>"FRITZ!Box 6660 Cable LQ",
|
||||
"conn"=>"w", # connection, w = wifi, m = mobile, o = offline
|
||||
"vac"=>3, # vertical accuracy
|
||||
"acc"=>5, # horizontal accuracy
|
||||
"tst"=>1702662679, Timestamp at which the beacon was seen (iOS/integer/epoch)
|
||||
"lat"=>52.445526,
|
||||
"m"=>1, # mode, significant = 1, move = 2
|
||||
"inrids"=>["5f1d1b"], # contains a list of region IDs the device is currently in (e.g. ["6da9cf","3defa7"]). Might be empty. (iOS,Android/list of strings/optional)
|
||||
"inregions"=>["home"],
|
||||
"point"=>{"bs"=>1,
|
||||
"p"=>102.818,
|
||||
"batt"=>100,
|
||||
"_type"=>"location",
|
||||
"tid"=>"RO",
|
||||
"topic"=>"owntracks/Frey/iPhone 12 Pro",
|
||||
"alt"=>36,
|
||||
"lon"=>13.504178,
|
||||
"vel"=>0,
|
||||
"t"=>"u",
|
||||
"BSSID"=>"b0:f2:8:45:94:33",
|
||||
"SSID"=>"FRITZ!Box 6660 Cable LQ",
|
||||
"conn"=>"w",
|
||||
"vac"=>3,
|
||||
"acc"=>5,
|
||||
"tst"=>1702662679,
|
||||
"lat"=>52.445526,
|
||||
"m"=>1,
|
||||
"inrids"=>["5f1d1b"],
|
||||
"inregions"=>["home"]}}
|
||||
18:51:18 web.1 | #<ActionController::Parameters {"bs"=>1,
|
||||
"p"=>102.818,
|
||||
"batt"=>100,
|
||||
"_type"=>"location",
|
||||
"tid"=>"RO",
|
||||
"topic"=>"owntracks/Frey/iPhone 12 Pro",
|
||||
"alt"=>36,
|
||||
"lon"=>13.504178,
|
||||
"vel"=>0,
|
||||
"t"=>"u",
|
||||
"BSSID"=>"b0:f2:8:45:94:33",
|
||||
"SSID"=>"FRITZ!Box 6660 Cable LQ",
|
||||
"conn"=>"w",
|
||||
"vac"=>3,
|
||||
"acc"=>5,
|
||||
"tst"=>1702662679,
|
||||
"lat"=>52.445526,
|
||||
"m"=>1,
|
||||
"inrids"=>["5f1d1b"],
|
||||
"inregions"=>["home"],
|
||||
"point"=>{"bs"=>1,
|
||||
"p"=>102.818,
|
||||
"batt"=>100,
|
||||
"_type"=>"location",
|
||||
"tid"=>"RO",
|
||||
"topic"=>"owntracks/Frey/iPhone 12 Pro",
|
||||
"alt"=>36,
|
||||
"lon"=>13.504178,
|
||||
"vel"=>0,
|
||||
"t"=>"u",
|
||||
"BSSID"=>"b0:f2:8:45:94:33",
|
||||
"SSID"=>"FRITZ!Box 6660 Cable LQ",
|
||||
"conn"=>"w",
|
||||
"vac"=>3,
|
||||
"acc"=>5,
|
||||
"tst"=>1702662679,
|
||||
"lat"=>52.445526,
|
||||
"m"=>1,
|
||||
"inrids"=>["5f1d1b"],
|
||||
"inregions"=>["home"]}} permitted: false>
|
||||
|
|
|
|||
24
app/controllers/api/v1/points_controller.rb
Normal file
24
app/controllers/api/v1/points_controller.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
class Api::V1::PointsController < ApplicationController
|
||||
skip_forgery_protection
|
||||
|
||||
def create
|
||||
parsed_params = OwnTracks::Params.new(point_params).call
|
||||
|
||||
@point = Point.create(parsed_params)
|
||||
|
||||
if @point.valid?
|
||||
render json: @point, status: :ok
|
||||
else
|
||||
render json: @point.errors, status: :unprocessable_entity
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def point_params
|
||||
params.permit(
|
||||
:lat, :lon, :bs, :batt, :p, :alt, :acc, :vac, :vel, :conn, :SSID, :BSSID, :m, :tid, :tst,
|
||||
:topic, :_type, :cog, :t, inrids: [], inregions: [], point: {}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,9 @@
|
|||
class HomeController < ApplicationController
|
||||
def index
|
||||
# if current_user
|
||||
# redirect_to dashboard_path
|
||||
# end
|
||||
if current_user
|
||||
redirect_to points_url
|
||||
end
|
||||
|
||||
@points = current_user.points if current_user
|
||||
end
|
||||
end
|
||||
|
|
|
|||
60
app/controllers/imports_controller.rb
Normal file
60
app/controllers/imports_controller.rb
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
class ImportsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :set_import, only: %i[ show destroy ]
|
||||
|
||||
def index
|
||||
@imports = current_user.imports
|
||||
end
|
||||
|
||||
def show
|
||||
end
|
||||
|
||||
def new
|
||||
@import = Import.new
|
||||
end
|
||||
|
||||
def create
|
||||
files = import_params[:files].reject(&:blank?)
|
||||
imports = []
|
||||
success = true
|
||||
|
||||
files.each do |file|
|
||||
json = JSON.parse(file.read)
|
||||
import = current_user.imports.create(name: file.original_filename)
|
||||
parser.new(file.path, import.id).call
|
||||
|
||||
imports << import
|
||||
end
|
||||
|
||||
redirect_to imports_url, notice: "#{imports.count} imports was successfully created.", status: :see_other
|
||||
|
||||
rescue StandardError => e
|
||||
imports.each { |import| import&.destroy! }
|
||||
|
||||
flash.now[:error] = e.message
|
||||
|
||||
redirect_to new_import_path, notice: e.message, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def destroy
|
||||
@import.destroy!
|
||||
redirect_to imports_url, notice: "Import was successfully destroyed.", status: :see_other
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_import
|
||||
@import = Import.find(params[:id])
|
||||
end
|
||||
|
||||
def import_params
|
||||
params.require(:import).permit(:source, files: [])
|
||||
end
|
||||
|
||||
def parser
|
||||
case params[:import][:source]
|
||||
when 'google' then GoogleMaps::TimelineParser
|
||||
when 'owntracks' then OwnTracks::ExportParser
|
||||
end
|
||||
end
|
||||
end
|
||||
7
app/controllers/points_controller.rb
Normal file
7
app/controllers/points_controller.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
class PointsController < ApplicationController
|
||||
def index
|
||||
@points = Point.all
|
||||
|
||||
@coordinates = @points.as_json(only: [:latitude, :longitude])
|
||||
end
|
||||
end
|
||||
2
app/helpers/points_helper.rb
Normal file
2
app/helpers/points_helper.rb
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
module PointsHelper
|
||||
end
|
||||
|
|
@ -3,3 +3,7 @@
|
|||
import "@rails/actioncable"
|
||||
import "controllers"
|
||||
import "@hotwired/turbo-rails"
|
||||
|
||||
import "mapkick/bundle"
|
||||
import "leaflet"
|
||||
import "leaflet-providers"
|
||||
|
|
|
|||
16
app/jobs/reverse_geocoding_job.rb
Normal file
16
app/jobs/reverse_geocoding_job.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
class ReverseGeocodingJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(point_id)
|
||||
point = Point.find(point_id)
|
||||
return if point.city.present? && point.country.present?
|
||||
|
||||
result = Geocoder.search([point.latitude, point.longitude])
|
||||
return if result.blank?
|
||||
|
||||
point.update(
|
||||
city: result.first.city,
|
||||
country: result.first.country
|
||||
)
|
||||
end
|
||||
end
|
||||
6
app/models/import.rb
Normal file
6
app/models/import.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
class Import < ApplicationRecord
|
||||
belongs_to :user, dependent: :destroy
|
||||
has_many :points, dependent: :destroy
|
||||
|
||||
enum source: { google: 0, owntracks: 1 }
|
||||
end
|
||||
42
app/models/point.rb
Normal file
42
app/models/point.rb
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
class Point < ApplicationRecord
|
||||
belongs_to :import, optional: true
|
||||
|
||||
validates :latitude, :longitude, :tracker_id, :timestamp, :topic, presence: true
|
||||
|
||||
enum battery_status: { unknown: 0, unplugged: 1, charging: 2, full: 3 }, _suffix: true
|
||||
enum trigger: {
|
||||
unknown: 0, background_event: 1, circular_region_event: 2, beacon_event: 3,
|
||||
report_location_message_event: 4, manual_event: 5, timer_based_event: 6,
|
||||
settings_monitoring_event: 7
|
||||
}, _suffix: true
|
||||
enum connection: { mobile: 0, wifi: 1, offline: 2 }, _suffix: true
|
||||
|
||||
after_create :async_reverse_geocode
|
||||
|
||||
def tracked_at
|
||||
Time.at(timestamp).strftime('%Y-%m-%d %H:%M:%S')
|
||||
end
|
||||
|
||||
def cities_by_countries
|
||||
group_by { _1.country }.compact.map { |k, v| { k => v.pluck(:city).uniq.compact } }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def async_reverse_geocode
|
||||
ReverseGeocodingJob.perform_later(id)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
def group_records_by_hour(records)
|
||||
grouped_records = Hash.new { |hash, key| hash[key] = [] }
|
||||
|
||||
records.each do |record|
|
||||
# Round timestamp to the nearest hour
|
||||
rounded_time = Time.at(record.timestamp).beginning_of_hour
|
||||
grouped_records[rounded_time] << record
|
||||
end
|
||||
|
||||
grouped_records
|
||||
end
|
||||
|
|
@ -3,4 +3,7 @@ class User < ApplicationRecord
|
|||
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
|
||||
devise :database_authenticatable, :registerable,
|
||||
:recoverable, :rememberable, :validatable
|
||||
|
||||
has_many :imports, dependent: :destroy
|
||||
has_many :points, through: :imports
|
||||
end
|
||||
|
|
|
|||
66
app/services/google_maps/timeline_parser.rb
Normal file
66
app/services/google_maps/timeline_parser.rb
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class GoogleMaps::TimelineParser
|
||||
attr_reader :file_path, :file, :json, :import_id
|
||||
|
||||
def initialize(file_path, import_id = nil)
|
||||
@file_path = file_path
|
||||
|
||||
raise 'File not found' unless File.exist?(@file_path)
|
||||
|
||||
@file = File.read(@file_path)
|
||||
@json = JSON.parse(@file)
|
||||
@import_id = import_id
|
||||
end
|
||||
|
||||
def call
|
||||
points_data = parse_json
|
||||
|
||||
points_data.each do |point_data|
|
||||
Point.create(
|
||||
latitude: point_data[:latitude],
|
||||
longitude: point_data[:longitude],
|
||||
timestamp: point_data[:timestamp],
|
||||
raw_data: point_data[:raw_data],
|
||||
topic: 'Google Maps Timeline Export',
|
||||
tracker_id: 'google-maps-timeline-export',
|
||||
import_id: import_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_json
|
||||
json['timelineObjects'].flat_map do |timeline_object|
|
||||
if timeline_object['activitySegment'].present?
|
||||
if timeline_object['activitySegment']['startLocation'].blank?
|
||||
next if timeline_object['activitySegment']['waypointPath'].blank?
|
||||
|
||||
timeline_object['activitySegment']['waypointPath']['waypoints'].map do |waypoint|
|
||||
{
|
||||
latitude: waypoint['latE7'].to_f / 10**7,
|
||||
longitude: waypoint['lngE7'].to_f / 10**7,
|
||||
timestamp: DateTime.parse(timeline_object['activitySegment']['duration']['startTimestamp']),
|
||||
raw_data: timeline_object
|
||||
}
|
||||
end
|
||||
else
|
||||
{
|
||||
latitude: timeline_object['activitySegment']['startLocation']['latitudeE7'].to_f / 10**7,
|
||||
longitude: timeline_object['activitySegment']['startLocation']['longitudeE7'].to_f / 10**7,
|
||||
timestamp: DateTime.parse(timeline_object['activitySegment']['duration']['startTimestamp']),
|
||||
raw_data: timeline_object
|
||||
}
|
||||
end
|
||||
elsif timeline_object['placeVisit'].present?
|
||||
{
|
||||
latitude: timeline_object['placeVisit']['location']['latitudeE7'].to_f / 10**7,
|
||||
longitude: timeline_object['placeVisit']['location']['longitudeE7'].to_f / 10**7,
|
||||
timestamp: DateTime.parse(timeline_object['placeVisit']['duration']['startTimestamp']),
|
||||
raw_data: timeline_object
|
||||
}
|
||||
end
|
||||
end.reject(&:blank?)
|
||||
end
|
||||
end
|
||||
45
app/services/own_tracks/export_parser.rb
Normal file
45
app/services/own_tracks/export_parser.rb
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OwnTracks::ExportParser
|
||||
attr_reader :file_path, :file, :json, :import_id
|
||||
|
||||
def initialize(file_path, import_id = nil)
|
||||
@file_path = file_path
|
||||
|
||||
raise 'File not found' unless File.exist?(@file_path)
|
||||
|
||||
@file = File.read(@file_path)
|
||||
@json = JSON.parse(@file)
|
||||
@import_id = import_id
|
||||
end
|
||||
|
||||
def call
|
||||
points_data = parse_json
|
||||
|
||||
points_data.each do |point_data|
|
||||
Point.create(
|
||||
latitude: point_data[:latitude],
|
||||
longitude: point_data[:longitude],
|
||||
timestamp: point_data[:timestamp],
|
||||
raw_data: point_data[:raw_data],
|
||||
topic: point_data[:topic],
|
||||
tracker_id: point_data[:tracker_id],
|
||||
import_id: import_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def parse_json
|
||||
points = []
|
||||
|
||||
json.keys.each do |user|
|
||||
json[user].keys.each do |devise|
|
||||
json[user][devise].each { |point| points << OwnTracks::Params.new(point).call }
|
||||
end
|
||||
end
|
||||
|
||||
points
|
||||
end
|
||||
end
|
||||
72
app/services/own_tracks/params.rb
Normal file
72
app/services/own_tracks/params.rb
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OwnTracks::Params
|
||||
attr_reader :params
|
||||
|
||||
def initialize(params)
|
||||
@params = params.deep_symbolize_keys
|
||||
end
|
||||
|
||||
def call
|
||||
{
|
||||
latitude: params[:lat],
|
||||
longitude: params[:lon],
|
||||
battery_status: battery_status,
|
||||
battery: params[:batt],
|
||||
ping: params[:p],
|
||||
altitude: params[:alt],
|
||||
accuracy: params[:acc],
|
||||
vertical_accuracy: params[:vac],
|
||||
velocity: params[:vel],
|
||||
connection: connection,
|
||||
ssid: params[:SSID],
|
||||
bssid: params[:BSSID],
|
||||
trigger: trigger,
|
||||
tracker_id: params[:tid],
|
||||
timestamp: Time.at(params[:tst].to_i),
|
||||
inrids: params[:inrids],
|
||||
in_regions: params[:inregions],
|
||||
topic: params[:topic],
|
||||
raw_data: params
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def battery_status
|
||||
return 'unknown' if params[:bs].nil?
|
||||
|
||||
case params[:bs]
|
||||
when 'u' then 'unplugged'
|
||||
when 'c' then 'charging'
|
||||
when 'f' then 'full'
|
||||
else 'unknown'
|
||||
end
|
||||
end
|
||||
|
||||
def trigger
|
||||
return 'unknown' if params[:m].nil?
|
||||
|
||||
case params[:m]
|
||||
when 'p' then 'background_event'
|
||||
when 'c' then 'circular_region_event'
|
||||
when 'b' then 'beacon_event'
|
||||
when 'r' then 'report_location_message_event'
|
||||
when 'u' then 'manual_event'
|
||||
when 't' then 'timer_based_event'
|
||||
when 'v' then 'settings_monitoring_event'
|
||||
else 'unknown'
|
||||
end
|
||||
end
|
||||
|
||||
def connection
|
||||
return 'mobile' if params[:conn].nil?
|
||||
|
||||
case params[:conn]
|
||||
when 'm' then 'mobile'
|
||||
when 'w' then 'wifi'
|
||||
when 'o' then 'offline'
|
||||
else 'unknown'
|
||||
end
|
||||
end
|
||||
end
|
||||
31
app/views/imports/_form.html.erb
Normal file
31
app/views/imports/_form.html.erb
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<%= 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>
|
||||
|
||||
<ul>
|
||||
<% import.errors.each do |error| %>
|
||||
<li><%= error.full_message %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<label class="form-control w-full max-w-xs my-5">
|
||||
<div class="label">
|
||||
<span class="label-text">Select source</span>
|
||||
</div>
|
||||
<%= form.collection_radio_buttons :source, Import.sources, :first, :first %>
|
||||
</label>
|
||||
|
||||
<label class="form-control w-full max-w-xs my-5">
|
||||
<div class="label">
|
||||
<span class="label-text">Pick a file</span>
|
||||
</div>
|
||||
<%= form.file_field :files, multiple: true, class: "file-input file-input-bordered w-full max-w-xs" %>
|
||||
</label>
|
||||
|
||||
<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 %>
|
||||
12
app/views/imports/_import.html.erb
Normal file
12
app/views/imports/_import.html.erb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
<div id="<%= dom_id import %>">
|
||||
<p class="my-5">
|
||||
<strong class="block font-medium mb-1">Name:</strong>
|
||||
<%= import.name %>
|
||||
</p>
|
||||
|
||||
<% if action_name != "show" %>
|
||||
<%= link_to "Show this import", import, class: "rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
<%= link_to "Edit this import", edit_import_path(import), class: "rounded-lg py-3 ml-2 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
<hr class="mt-6">
|
||||
<% end %>
|
||||
</div>
|
||||
8
app/views/imports/edit.html.erb
Normal file
8
app/views/imports/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 import</h1>
|
||||
|
||||
<%= render "form", import: @import %>
|
||||
|
||||
<%= link_to "Show this import", @import, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
<%= link_to "Back to imports", imports_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
</div>
|
||||
10
app/views/imports/index.html.erb
Normal file
10
app/views/imports/index.html.erb
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<div class="w-full">
|
||||
<div class="flex justify-between items-center">
|
||||
<h1 class="font-bold text-4xl">Imports</h1>
|
||||
<%= link_to "New import", new_import_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
|
||||
</div>
|
||||
|
||||
<div id="imports" class="min-w-full">
|
||||
<%= render @imports %>
|
||||
</div>
|
||||
</div>
|
||||
7
app/views/imports/new.html.erb
Normal file
7
app/views/imports/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 import</h1>
|
||||
|
||||
<%= render "form", import: @import %>
|
||||
|
||||
<%= link_to "Back to imports", imports_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
</div>
|
||||
15
app/views/imports/show.html.erb
Normal file
15
app/views/imports/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 @import %>
|
||||
|
||||
<%= link_to "Edit this import", edit_import_path(@import), class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
<div class="inline-block ml-2">
|
||||
<%= button_to "Destroy this import", import_path(@import), method: :delete, class: "mt-2 rounded-lg py-3 px-5 bg-gray-100 font-medium" %>
|
||||
</div>
|
||||
<%= link_to "Back to imports", imports_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
|
||||
</div>
|
||||
</div>
|
||||
61
app/views/points/_table.html.erb
Normal file
61
app/views/points/_table.html.erb
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
<h1 class="text-2xl font-bold mb-4">Points Table</h1>
|
||||
|
||||
<table class="min-w-full bg-white border border-gray-300">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="py-2 px-4 border-b">ID</th>
|
||||
<th class="py-2 px-4 border-b">Battery Status</th>
|
||||
<th class="py-2 px-4 border-b">Ping</th>
|
||||
<th class="py-2 px-4 border-b">Battery</th>
|
||||
<th class="py-2 px-4 border-b">Tracked ID</th>
|
||||
<th class="py-2 px-4 border-b">Topic</th>
|
||||
<th class="py-2 px-4 border-b">Altitude</th>
|
||||
<th class="py-2 px-4 border-b">Velocity</th>
|
||||
<th class="py-2 px-4 border-b">Trigger</th>
|
||||
<th class="py-2 px-4 border-b">BSSID</th>
|
||||
<th class="py-2 px-4 border-b">SSID</th>
|
||||
<th class="py-2 px-4 border-b">Connection</th>
|
||||
<th class="py-2 px-4 border-b">Vertical Accuracy</th>
|
||||
<th class="py-2 px-4 border-b">Accuracy</th>
|
||||
<th class="py-2 px-4 border-b">Timestamp</th>
|
||||
<th class="py-2 px-4 border-b">Mode</th>
|
||||
<th class="py-2 px-4 border-b">Latitude</th>
|
||||
<th class="py-2 px-4 border-b">Longitude</th>
|
||||
<th class="py-2 px-4 border-b">Inrids</th>
|
||||
<th class="py-2 px-4 border-b">In Regions</th>
|
||||
<th class="py-2 px-4 border-b">Raw Data</th>
|
||||
<th class="py-2 px-4 border-b">Tracker ID</th>
|
||||
<th class="py-2 px-4 border-b">Created At</th>
|
||||
<th class="py-2 px-4 border-b">Updated At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @points.each do |point| %>
|
||||
<tr>
|
||||
<td class="py-2 px-4 border-b"><%= point.id %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.battery_status %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.ping %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.battery %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.tracker_id %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.topic %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.altitude %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.velocity %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.trigger %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.bssid %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.ssid %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.connection %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.vertical_accuracy %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.accuracy %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.timestamp %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.mode %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.latitude %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.longitude %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.inrids %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.in_regions %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.raw_data %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.created_at %></td>
|
||||
<td class="py-2 px-4 border-b"><%= point.updated_at %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
3
app/views/points/index.html.erb
Normal file
3
app/views/points/index.html.erb
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<%= js_map(@coordinates, style: "mapbox://styles/mapbox/outdoors-v12", trail: true) %>
|
||||
|
||||
<%#= render 'points/table', points: @points %>
|
||||
|
|
@ -5,7 +5,8 @@
|
|||
<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 'Test url', 'portfolios_url' %></li>
|
||||
<li><%= link_to 'Imports', imports_url %></li>
|
||||
<li><%= link_to 'Points', points_url %></li>
|
||||
</ul>
|
||||
</div>
|
||||
<%= link_to 'Dawarich', root_path, class: 'btn btn-ghost normal-case text-xl'%>
|
||||
|
|
@ -17,23 +18,26 @@
|
|||
</div>
|
||||
<div class="navbar-center hidden lg:flex">
|
||||
<ul class="menu menu-horizontal px-1">
|
||||
<li><%= link_to 'Test url', 'portfolios_url' %></li>
|
||||
<li><%= link_to 'Imports', imports_url %></li>
|
||||
<li><%= link_to 'Points', points_url %></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="navbar-end">
|
||||
<%# menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52 %>
|
||||
<ul class="menu menu-horizontal dropdown-content bg-base-100 rounded-box px-1">
|
||||
<ul class="menu menu-horizontal bg-base-100 rounded-box px-1">
|
||||
<% if user_signed_in? %>
|
||||
<li tabindex="0">
|
||||
<a>
|
||||
<li>
|
||||
<details>
|
||||
<summary>
|
||||
<%= "#{current_user.email}" %>
|
||||
<svg class="fill-current" xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"><path d="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z"/></svg>
|
||||
</a>
|
||||
<ul class="p-2 bg-base-300">
|
||||
</summary>
|
||||
<ul class="p-2 bg-base-100 rounded-t-none">
|
||||
<li><%= link_to 'Settings', edit_user_registration_path %></li>
|
||||
<li><%= link_to 'Logout', destroy_user_session_path, method: :delete, data: { turbo_method: :delete } %></li>
|
||||
</ul>
|
||||
</details>
|
||||
</li>
|
||||
</details>
|
||||
<% else %>
|
||||
<li><%= link_to 'Login', new_user_session_path %></li>
|
||||
<li><%= link_to 'Register', new_user_registration_path %></li>
|
||||
|
|
|
|||
|
|
@ -8,3 +8,7 @@ pin "@hotwired/turbo-rails", to: "turbo.min.js", preload: true
|
|||
pin "@hotwired/stimulus", to: "stimulus.min.js", preload: true
|
||||
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
|
||||
pin_all_from "app/javascript/controllers", under: "controllers"
|
||||
|
||||
pin "mapkick/bundle", to: "mapkick.bundle.js"
|
||||
pin "leaflet", to: "https://ga.jspm.io/npm:leaflet@1.9.4/dist/leaflet-src.js"
|
||||
pin "leaflet-providers", to: "https://ga.jspm.io/npm:leaflet-providers@2.0.0/leaflet-providers.js"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,14 @@
|
|||
Rails.application.routes.draw do
|
||||
get 'points/index'
|
||||
resources :imports
|
||||
root to: 'home#index'
|
||||
devise_for :users
|
||||
|
||||
get 'points', to: 'points#index'
|
||||
|
||||
namespace :api do
|
||||
namespace :v1 do
|
||||
resources :points
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
39
db/migrate/20240315213523_create_points.rb
Normal file
39
db/migrate/20240315213523_create_points.rb
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
class CreatePoints < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
create_table :points do |t|
|
||||
t.integer :battery_status
|
||||
t.string :ping
|
||||
t.integer :battery
|
||||
t.string :tracker_id
|
||||
t.string :topic
|
||||
t.integer :altitude
|
||||
t.decimal :longitude, precision: 10, scale: 6
|
||||
t.string :velocity
|
||||
t.integer :trigger
|
||||
t.string :bssid
|
||||
t.string :ssid
|
||||
t.integer :connection
|
||||
t.integer :vertical_accuracy
|
||||
t.integer :accuracy
|
||||
t.integer :timestamp
|
||||
t.decimal :latitude, precision: 10, scale: 6
|
||||
t.integer :mode
|
||||
t.text :inrids, array: true, default: []
|
||||
t.text :in_regions, array: true, default: []
|
||||
t.jsonb :raw_data, default: {}
|
||||
t.bigint :import_id
|
||||
t.string :city
|
||||
t.string :country
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :points, :battery_status
|
||||
add_index :points, :battery
|
||||
add_index :points, :altitude
|
||||
add_index :points, :trigger
|
||||
add_index :points, :connection
|
||||
add_index :points, :import_id
|
||||
add_index :points, :city
|
||||
add_index :points, :country
|
||||
end
|
||||
end
|
||||
13
db/migrate/20240315215423_create_imports.rb
Normal file
13
db/migrate/20240315215423_create_imports.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
class CreateImports < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
create_table :imports do |t|
|
||||
t.string :name, null: false
|
||||
t.bigint :user_id, null: false
|
||||
t.integer :source, default: 0
|
||||
|
||||
t.timestamps
|
||||
end
|
||||
add_index :imports, :user_id
|
||||
add_index :imports, :source
|
||||
end
|
||||
end
|
||||
48
db/schema.rb
generated
48
db/schema.rb
generated
|
|
@ -10,10 +10,56 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2023_10_21_104258) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_03_15_215423) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
create_table "imports", force: :cascade do |t|
|
||||
t.string "name", null: false
|
||||
t.bigint "user_id", null: false
|
||||
t.integer "source", default: 0
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["source"], name: "index_imports_on_source"
|
||||
t.index ["user_id"], name: "index_imports_on_user_id"
|
||||
end
|
||||
|
||||
create_table "points", force: :cascade do |t|
|
||||
t.integer "battery_status"
|
||||
t.string "ping"
|
||||
t.integer "battery"
|
||||
t.string "tracker_id"
|
||||
t.string "topic"
|
||||
t.integer "altitude"
|
||||
t.decimal "longitude", precision: 10, scale: 6
|
||||
t.string "velocity"
|
||||
t.integer "trigger"
|
||||
t.string "bssid"
|
||||
t.string "ssid"
|
||||
t.integer "connection"
|
||||
t.integer "vertical_accuracy"
|
||||
t.integer "accuracy"
|
||||
t.integer "timestamp"
|
||||
t.decimal "latitude", precision: 10, scale: 6
|
||||
t.integer "mode"
|
||||
t.text "inrids", default: [], array: true
|
||||
t.text "in_regions", default: [], array: true
|
||||
t.jsonb "raw_data", default: {}
|
||||
t.bigint "import_id"
|
||||
t.string "city"
|
||||
t.string "country"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.index ["altitude"], name: "index_points_on_altitude"
|
||||
t.index ["battery"], name: "index_points_on_battery"
|
||||
t.index ["battery_status"], name: "index_points_on_battery_status"
|
||||
t.index ["city"], name: "index_points_on_city"
|
||||
t.index ["connection"], name: "index_points_on_connection"
|
||||
t.index ["country"], name: "index_points_on_country"
|
||||
t.index ["import_id"], name: "index_points_on_import_id"
|
||||
t.index ["trigger"], name: "index_points_on_trigger"
|
||||
end
|
||||
|
||||
create_table "users", force: :cascade do |t|
|
||||
t.string "email", default: "", null: false
|
||||
t.string "encrypted_password", default: "", null: false
|
||||
|
|
|
|||
6
spec/factories/imports.rb
Normal file
6
spec/factories/imports.rb
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
FactoryBot.define do
|
||||
factory :import do
|
||||
user_id { "" }
|
||||
source { 1 }
|
||||
end
|
||||
end
|
||||
27
spec/factories/points.rb
Normal file
27
spec/factories/points.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
FactoryBot.define do
|
||||
factory :point do
|
||||
battery_status { 1 }
|
||||
ping { "MyString" }
|
||||
battery { 1 }
|
||||
topic { "MyString" }
|
||||
altitude { 1 }
|
||||
longitude { "MyString" }
|
||||
velocity { "MyString" }
|
||||
trigger { 1 }
|
||||
bssid { "MyString" }
|
||||
ssid { "MyString" }
|
||||
connection { 1 }
|
||||
vertical_accuracy { 1 }
|
||||
accuracy { 1 }
|
||||
timestamp { 1 }
|
||||
latitude { "MyString" }
|
||||
mode { 1 }
|
||||
inrids { "MyString" }
|
||||
in_regions { "MyString" }
|
||||
raw_data { "" }
|
||||
tracker_id { "MyString" }
|
||||
import_id { "" }
|
||||
city { "MyString" }
|
||||
country { "MyString" }
|
||||
end
|
||||
end
|
||||
5
spec/models/import_spec.rb
Normal file
5
spec/models/import_spec.rb
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Import, type: :model do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
end
|
||||
15
spec/models/point_spec.rb
Normal file
15
spec/models/point_spec.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Point, type: :model do
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:tracker).optional }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
it { is_expected.to validate_presence_of(:latitude) }
|
||||
it { is_expected.to validate_presence_of(:longitude) }
|
||||
it { is_expected.to validate_presence_of(:tracker_id) }
|
||||
it { is_expected.to validate_presence_of(:timestamp) }
|
||||
it { is_expected.to validate_presence_of(:topic) }
|
||||
end
|
||||
end
|
||||
30
spec/requests/api/v1/points_spec.rb
Normal file
30
spec/requests/api/v1/points_spec.rb
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe "Api::V1::Points", type: :request do
|
||||
describe "POST /api/v1/points" do
|
||||
context 'with valid params' do
|
||||
let(:params) do
|
||||
{ lat: 1.0, lon: 1.0, tid: 'test', tst: Time.now.to_i, topic: 'iPhone 12 pro' }
|
||||
end
|
||||
|
||||
it "returns http success" do
|
||||
post api_v1_points_path, params: params
|
||||
|
||||
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
|
||||
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\"]}")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
11
spec/requests/points_spec.rb
Normal file
11
spec/requests/points_spec.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
require 'rails_helper'
|
||||
|
||||
RSpec.describe "Points", type: :request do
|
||||
describe "GET /index" do
|
||||
it "returns http success" do
|
||||
get "/points/index"
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
Loading…
Reference in a new issue