Migrate from old template

This commit is contained in:
Eugene Burmakin 2024-03-15 23:27:31 +01:00
parent f74415217c
commit 3c74bc2937
38 changed files with 847 additions and 22 deletions

View file

@ -1,4 +1,4 @@
DATABASE_HOST=dawarich_db DATABASE_HOST=localhost
DATABASE_USERNAME=postgres DATABASE_USERNAME=postgres
DATABASE_PASSWORD=password DATABASE_PASSWORD=password
DATABASE_NAME=dawarich_development DATABASE_NAME=dawarich_development

1
.env.template Normal file
View file

@ -0,0 +1 @@
MAPBOX_ACCESS_TOKEN=MAPBOX_ACCESS_TOKEN

View file

@ -17,12 +17,17 @@ gem 'tailwindcss-rails'
gem 'turbo-rails' gem 'turbo-rails'
gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby]
gem "importmap-rails" gem "importmap-rails"
gem "mapkick-rb"
gem 'geocoder'
gem 'sidekiq'
group :development, :test do group :development, :test do
gem 'debug', platforms: %i[mri mingw x64_mingw] gem 'debug', platforms: %i[mri mingw x64_mingw]
gem 'factory_bot_rails' gem 'factory_bot_rails'
gem 'ffaker' gem 'ffaker'
gem 'rspec-rails' gem 'rspec-rails'
gem 'dotenv-rails'
end end
group :test do group :test do

View file

@ -97,6 +97,10 @@ GEM
warden (~> 1.2.3) warden (~> 1.2.3)
diff-lcs (1.5.1) diff-lcs (1.5.1)
docile (1.4.0) docile (1.4.0)
dotenv (3.1.0)
dotenv-rails (3.1.0)
dotenv (= 3.1.0)
railties (>= 6.1)
drb (2.2.1) drb (2.2.1)
erubi (1.12.0) erubi (1.12.0)
factory_bot (6.4.6) factory_bot (6.4.6)
@ -106,6 +110,7 @@ GEM
railties (>= 5.0.0) railties (>= 5.0.0)
ffaker (2.23.0) ffaker (2.23.0)
foreman (0.87.2) foreman (0.87.2)
geocoder (1.8.2)
globalid (1.2.1) globalid (1.2.1)
activesupport (>= 6.1) activesupport (>= 6.1)
i18n (1.14.4) i18n (1.14.4)
@ -128,6 +133,7 @@ GEM
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
mapkick-rb (0.1.5)
marcel (1.0.4) marcel (1.0.4)
mini_mime (1.1.5) mini_mime (1.1.5)
minitest (5.22.3) minitest (5.22.3)
@ -252,6 +258,11 @@ GEM
ruby-progressbar (1.13.0) ruby-progressbar (1.13.0)
shoulda-matchers (6.1.0) shoulda-matchers (6.1.0)
activesupport (>= 5.2.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) simplecov (0.22.0)
docile (~> 1.1) docile (~> 1.1)
simplecov-html (~> 0.11) simplecov-html (~> 0.11)
@ -305,10 +316,13 @@ DEPENDENCIES
bootsnap bootsnap
debug debug
devise devise
dotenv-rails
factory_bot_rails factory_bot_rails
ffaker ffaker
foreman foreman
geocoder
importmap-rails importmap-rails
mapkick-rb
pg pg
puma puma
pundit pundit
@ -317,6 +331,7 @@ DEPENDENCIES
rspec-rails rspec-rails
rubocop-rails rubocop-rails
shoulda-matchers shoulda-matchers
sidekiq
simplecov simplecov
sprockets-rails sprockets-rails
stimulus-rails stimulus-rails

114
README.md
View file

@ -1,17 +1,16 @@
# Dawarich # 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 You can import your Google Maps Timeline data into Wardu.
ruby rename_app.rb old_app_name new_app_name
```
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 ## How to start the app locally
@ -40,7 +39,7 @@ Dockerized with https://betterprogramming.pub/rails-6-development-with-docker-55
```json ```json
{ {
"scripts": { "scripts": {
"predeploy": "dokku ps:stop dawarich" "predeploy": "dokku ps:stop wardu"
}, },
"formation": { "formation": {
"web": { "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>

View 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

View file

@ -1,7 +1,9 @@
class HomeController < ApplicationController class HomeController < ApplicationController
def index def index
# if current_user if current_user
# redirect_to dashboard_path redirect_to points_url
# end end
@points = current_user.points if current_user
end end
end end

View 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

View file

@ -0,0 +1,7 @@
class PointsController < ApplicationController
def index
@points = Point.all
@coordinates = @points.as_json(only: [:latitude, :longitude])
end
end

View file

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

View file

@ -3,3 +3,7 @@
import "@rails/actioncable" import "@rails/actioncable"
import "controllers" import "controllers"
import "@hotwired/turbo-rails" import "@hotwired/turbo-rails"
import "mapkick/bundle"
import "leaflet"
import "leaflet-providers"

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

View file

@ -3,4 +3,7 @@ class User < ApplicationRecord
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable, devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable :recoverable, :rememberable, :validatable
has_many :imports, dependent: :destroy
has_many :points, through: :imports
end end

View 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

View 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

View 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

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

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

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

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

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

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

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

View file

@ -0,0 +1,3 @@
<%= js_map(@coordinates, style: "mapbox://styles/mapbox/outdoors-v12", trail: true) %>
<%#= render 'points/table', points: @points %>

View file

@ -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> <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> </label>
<ul tabindex="0" class="menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52"> <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> </ul>
</div> </div>
<%= link_to 'Dawarich', root_path, class: 'btn btn-ghost normal-case text-xl'%> <%= link_to 'Dawarich', root_path, class: 'btn btn-ghost normal-case text-xl'%>
@ -17,23 +18,26 @@
</div> </div>
<div class="navbar-center hidden lg:flex"> <div class="navbar-center hidden lg:flex">
<ul class="menu menu-horizontal px-1"> <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> </ul>
</div> </div>
<div class="navbar-end"> <div class="navbar-end">
<%# menu menu-sm dropdown-content mt-3 z-[1] p-2 shadow bg-base-100 rounded-box w-52 %> <%# 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? %> <% if user_signed_in? %>
<li tabindex="0"> <li>
<a> <details>
<summary>
<%= "#{current_user.email}" %> <%= "#{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> </summary>
</a> <ul class="p-2 bg-base-100 rounded-t-none">
<ul class="p-2 bg-base-300">
<li><%= link_to 'Settings', edit_user_registration_path %></li> <li><%= link_to 'Settings', edit_user_registration_path %></li>
<li><%= link_to 'Logout', destroy_user_session_path, method: :delete, data: { turbo_method: :delete } %></li> <li><%= link_to 'Logout', destroy_user_session_path, method: :delete, data: { turbo_method: :delete } %></li>
</ul> </ul>
</li> </details>
</li>
</details>
<% else %> <% else %>
<li><%= link_to 'Login', new_user_session_path %></li> <li><%= link_to 'Login', new_user_session_path %></li>
<li><%= link_to 'Register', new_user_registration_path %></li> <li><%= link_to 'Register', new_user_registration_path %></li>

View file

@ -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", to: "stimulus.min.js", preload: true
pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true pin "@hotwired/stimulus-loading", to: "stimulus-loading.js", preload: true
pin_all_from "app/javascript/controllers", under: "controllers" 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"

View file

@ -1,4 +1,14 @@
Rails.application.routes.draw do Rails.application.routes.draw do
get 'points/index'
resources :imports
root to: 'home#index' root to: 'home#index'
devise_for :users devise_for :users
get 'points', to: 'points#index'
namespace :api do
namespace :v1 do
resources :points
end
end
end end

View 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

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

@ -10,10 +10,56 @@
# #
# It's strongly recommended that you check this file into your version control system. # 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 # These are extensions that must be enabled in order to support this database
enable_extension "plpgsql" 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| create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false t.string "encrypted_password", default: "", null: false

View file

@ -0,0 +1,6 @@
FactoryBot.define do
factory :import do
user_id { "" }
source { 1 }
end
end

27
spec/factories/points.rb Normal file
View 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

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

View 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

View 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