mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
commit
fdf7d6f4a0
51 changed files with 350 additions and 99 deletions
|
|
@ -1 +1 @@
|
|||
0.3.2
|
||||
0.4.0
|
||||
|
|
|
|||
30
CHANGELOG.md
30
CHANGELOG.md
|
|
@ -5,6 +5,36 @@ 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.4.0] — 2024-05-25
|
||||
|
||||
**BREAKING CHANGES**:
|
||||
|
||||
- `/api/v1/points` is still working, but will be **deprecated** in nearest future. Please use `/api/v1/owntracks/points` instead.
|
||||
- All existing points recorded directly to the database via Owntracks or Overland will be attached to the user with id 1.
|
||||
|
||||
### Added
|
||||
|
||||
- Each user now have an api key, which is required to make requests to the API. You can find your api key in your profile settings.
|
||||
- You can re-generate your api key in your profile settings.
|
||||
- In your user profile settings you can now see the instructions on how to use the API with your api key for both OwnTracks and Overland.
|
||||
- Added docs on how to use the API with your api key. Refer to `/api-docs` for more information.
|
||||
- `POST /api/v1/owntracks/points` endpoint.
|
||||
- Points are now being attached to a user directly, so you can only see your own points and no other users of your applications can see your points.
|
||||
|
||||
### Changed
|
||||
|
||||
- `/api/v1/overland/batches` endpoint now requires an api key to be passed in the url. You can find your api key in your profile settings.
|
||||
- All existing points recorded directly to the database will be attached to the user with id 1.
|
||||
- All stats and maps are now being calculated and rendered based on the user's points only.
|
||||
- Default `TIME_ZONE` environment variable is now set to 'UTC' in the `docker-compose.yml` file.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed a bug where marker on the map was rendering timestamp without considering the timezone.
|
||||
|
||||
---
|
||||
|
||||
|
||||
## [0.3.2] — 2024-05-23
|
||||
|
||||
### Added
|
||||
|
|
|
|||
1
Gemfile
1
Gemfile
|
|
@ -6,6 +6,7 @@ git_source(:github) { |repo| "https://github.com/#{repo}.git" }
|
|||
ruby '3.2.3'
|
||||
gem 'bootsnap', require: false
|
||||
gem 'chartkick'
|
||||
gem 'data_migrate'
|
||||
gem 'devise'
|
||||
gem 'geocoder'
|
||||
gem 'importmap-rails'
|
||||
|
|
|
|||
|
|
@ -96,6 +96,9 @@ GEM
|
|||
rexml
|
||||
crass (1.0.6)
|
||||
csv (3.3.0)
|
||||
data_migrate (9.4.0)
|
||||
activerecord (>= 6.1)
|
||||
railties (>= 6.1)
|
||||
date (3.3.4)
|
||||
debug (1.9.2)
|
||||
irb (~> 1.10)
|
||||
|
|
@ -391,6 +394,7 @@ PLATFORMS
|
|||
DEPENDENCIES
|
||||
bootsnap
|
||||
chartkick
|
||||
data_migrate
|
||||
debug
|
||||
devise
|
||||
dotenv-rails
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -2,9 +2,10 @@
|
|||
|
||||
class Api::V1::Overland::BatchesController < ApplicationController
|
||||
skip_forgery_protection
|
||||
before_action :authenticate_api_key
|
||||
|
||||
def create
|
||||
Overland::BatchCreatingJob.perform_later(batch_params)
|
||||
Overland::BatchCreatingJob.perform_later(batch_params, current_api_user.id)
|
||||
|
||||
render json: { result: 'ok' }, status: :created
|
||||
end
|
||||
|
|
@ -12,6 +13,6 @@ class Api::V1::Overland::BatchesController < ApplicationController
|
|||
private
|
||||
|
||||
def batch_params
|
||||
params.permit(locations: [:type, geometry: {}, properties: {}], batch: {})
|
||||
params.permit(locations: [:type, { geometry: {}, properties: {} }], batch: {})
|
||||
end
|
||||
end
|
||||
|
|
|
|||
18
app/controllers/api/v1/owntracks/points_controller.rb
Normal file
18
app/controllers/api/v1/owntracks/points_controller.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Owntracks::PointsController < ApplicationController
|
||||
skip_forgery_protection
|
||||
before_action :authenticate_api_key
|
||||
|
||||
def create
|
||||
Owntracks::PointCreatingJob.perform_later(point_params, current_api_user.id)
|
||||
|
||||
render json: {}, status: :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def point_params
|
||||
params.permit!
|
||||
end
|
||||
end
|
||||
|
|
@ -1,21 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
# TODO: Deprecate in 1.0
|
||||
|
||||
class Api::V1::PointsController < ApplicationController
|
||||
skip_forgery_protection
|
||||
|
||||
def create
|
||||
Rails.logger.info 'This endpoint will be deprecated in 1.0. Use /api/v1/owntracks/points instead'
|
||||
Owntracks::PointCreatingJob.perform_later(point_params)
|
||||
|
||||
render json: {}, status: :ok
|
||||
end
|
||||
|
||||
def destroy
|
||||
@point = Point.find(params[:id])
|
||||
@point.destroy
|
||||
|
||||
head :no_content
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def point_params
|
||||
|
|
|
|||
|
|
@ -1,3 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class ApplicationController < ActionController::Base
|
||||
include Pundit::Authorization
|
||||
|
||||
protected
|
||||
|
||||
def authenticate_api_key
|
||||
return head :unauthorized unless current_api_user
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def current_api_user
|
||||
@current_api_user ||= User.find_by(api_key: params[:api_key])
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class ExportController < ApplicationController
|
|||
end
|
||||
|
||||
def start_at
|
||||
first_point_timestamp = Point.order(timestamp: :asc)&.first&.timestamp
|
||||
first_point_timestamp = current_user.tracked_points.order(timestamp: :asc)&.first&.timestamp
|
||||
|
||||
@start_at ||=
|
||||
if params[:start_at].nil? && first_point_timestamp.present?
|
||||
|
|
@ -37,7 +37,7 @@ class ExportController < ApplicationController
|
|||
end
|
||||
|
||||
def end_at
|
||||
last_point_timestamp = Point.order(timestamp: :desc)&.last&.timestamp
|
||||
last_point_timestamp = current_user.tracked_points.order(timestamp: :desc)&.last&.timestamp
|
||||
|
||||
@end_at ||=
|
||||
if params[:end_at].nil? && last_point_timestamp.present?
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@ class HomeController < ApplicationController
|
|||
def index
|
||||
redirect_to map_url if current_user
|
||||
|
||||
@points = current_user.points.without_raw_data if current_user
|
||||
@points = current_user.tracked_points.without_raw_data if current_user
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ 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)
|
||||
@points = current_user.tracked_points.without_raw_data.where('timestamp >= ? AND timestamp <= ?', start_at, end_at).order(timestamp: :asc)
|
||||
|
||||
@countries_and_cities = CountriesAndCities.new(@points).call
|
||||
@coordinates =
|
||||
|
|
|
|||
|
|
@ -5,7 +5,8 @@ class PointsController < ApplicationController
|
|||
|
||||
def index
|
||||
@points =
|
||||
Point
|
||||
current_user
|
||||
.tracked_points
|
||||
.without_raw_data
|
||||
.where('timestamp >= ? AND timestamp <= ?', start_at, end_at)
|
||||
.order(timestamp: :asc)
|
||||
|
|
@ -16,9 +17,9 @@ class PointsController < ApplicationController
|
|||
end
|
||||
|
||||
def bulk_destroy
|
||||
Point.where(id: params[:point_ids].compact).destroy_all
|
||||
current_user.tracked_points.where(id: params[:point_ids].compact).destroy_all
|
||||
|
||||
redirect_to points_url, notice: "Points were successfully destroyed.", status: :see_other
|
||||
redirect_to points_url, notice: 'Points were successfully destroyed.', status: :see_other
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -8,4 +8,10 @@ class SettingsController < ApplicationController
|
|||
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
|
||||
def generate_api_key
|
||||
current_user.update(api_key: SecureRandom.hex)
|
||||
|
||||
redirect_back(fallback_location: root_path)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -37,24 +37,24 @@ module ApplicationHelper
|
|||
%w[info success warning error accent secondary primary]
|
||||
end
|
||||
|
||||
def countries_and_cities_stat(year)
|
||||
data = Stat.year_cities_and_countries(year)
|
||||
def countries_and_cities_stat(year, user)
|
||||
data = Stat.year_cities_and_countries(year, user)
|
||||
countries = data[:countries]
|
||||
cities = data[:cities]
|
||||
|
||||
"#{countries} countries, #{cities} cities"
|
||||
end
|
||||
|
||||
def year_distance_stat_in_km(year)
|
||||
Stat.year_distance(year).sum { _1[1] }
|
||||
def year_distance_stat_in_km(year, user)
|
||||
Stat.year_distance(year, user).sum { _1[1] }
|
||||
end
|
||||
|
||||
def past?(year, month)
|
||||
DateTime.new(year, month).past?
|
||||
end
|
||||
|
||||
def points_exist?(year, month)
|
||||
Point.where(
|
||||
def points_exist?(year, month, user)
|
||||
user.tracked_points.where(
|
||||
timestamp: DateTime.new(year, month).beginning_of_month..DateTime.new(year, month).end_of_month
|
||||
).exists?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -96,15 +96,9 @@ export default class extends Controller {
|
|||
formatDate(timestamp) {
|
||||
let date = new Date(timestamp * 1000); // Multiply by 1000 because JavaScript works with milliseconds
|
||||
|
||||
// Extracting date components
|
||||
let year = date.getFullYear();
|
||||
let month = ('0' + (date.getMonth() + 1)).slice(-2); // Adding 1 because getMonth() returns zero-based month
|
||||
let day = ('0' + date.getDate()).slice(-2);
|
||||
let hours = ('0' + date.getHours()).slice(-2);
|
||||
let minutes = ('0' + date.getMinutes()).slice(-2);
|
||||
let seconds = ('0' + date.getSeconds()).slice(-2);
|
||||
let timezone = this.element.dataset.timezone;
|
||||
|
||||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||||
return date.toLocaleString('en-GB', { timeZone: timezone });
|
||||
}
|
||||
|
||||
addTileLayer(map) {
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ class ImportJob < ApplicationJob
|
|||
user = User.find(user_id)
|
||||
import = user.imports.find(import_id)
|
||||
|
||||
result = parser(import.source).new(import).call
|
||||
result = parser(import.source).new(import, user_id).call
|
||||
|
||||
import.update(
|
||||
raw_points: result[:raw_points], doubles: result[:doubles], processed: result[:processed]
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
class Overland::BatchCreatingJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(params)
|
||||
def perform(params, user_id)
|
||||
data = Overland::Params.new(params).call
|
||||
|
||||
data.each do |location|
|
||||
Point.create!(location)
|
||||
Point.create!(location.merge(user_id:))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -3,9 +3,10 @@
|
|||
class Owntracks::PointCreatingJob < ApplicationJob
|
||||
queue_as :default
|
||||
|
||||
def perform(point_params)
|
||||
# TODO: after deprecation of old endpoint, make user_id required
|
||||
def perform(point_params, user_id = nil)
|
||||
parsed_params = OwnTracks::Params.new(point_params).call
|
||||
|
||||
Point.create(parsed_params)
|
||||
Point.create!(parsed_params.merge(user_id:))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ class Point < ApplicationRecord
|
|||
# self.ignored_columns = %w[raw_data]
|
||||
|
||||
belongs_to :import, optional: true
|
||||
belongs_to :user, optional: true
|
||||
|
||||
validates :latitude, :longitude, :timestamp, presence: true
|
||||
|
||||
|
|
|
|||
|
|
@ -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.without_raw_data.where(timestamp: beginning_of_day..end_of_day)
|
||||
points = user.tracked_points.without_raw_data.where(timestamp: beginning_of_day..end_of_day)
|
||||
|
||||
data = { day: index, distance: 0 }
|
||||
|
||||
|
|
@ -27,8 +27,8 @@ class Stat < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def self.year_distance(year)
|
||||
stats = where(year: year).order(:month)
|
||||
def self.year_distance(year, user)
|
||||
stats = where(year:, user:).order(:month)
|
||||
|
||||
(1..12).to_a.map do |month|
|
||||
month_stat = stats.select { |stat| stat.month == month }.first
|
||||
|
|
@ -40,8 +40,8 @@ class Stat < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def self.year_cities_and_countries(year)
|
||||
points = Point.where(timestamp: DateTime.new(year).beginning_of_year..DateTime.new(year).end_of_year)
|
||||
def self.year_cities_and_countries(year, user)
|
||||
points = user.tracked_points.where(timestamp: DateTime.new(year).beginning_of_year..DateTime.new(year).end_of_year)
|
||||
|
||||
data = CountriesAndCities.new(points).call
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ class User < ApplicationRecord
|
|||
has_many :imports, dependent: :destroy
|
||||
has_many :points, through: :imports
|
||||
has_many :stats, dependent: :destroy
|
||||
has_many :tracked_points, class_name: 'Point', dependent: :destroy
|
||||
|
||||
after_create :create_api_key
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ class CreateStats
|
|||
beginning_of_month_timestamp = DateTime.new(year, month).beginning_of_month.to_i
|
||||
end_of_month_timestamp = DateTime.new(year, month).end_of_month.to_i
|
||||
|
||||
points = points(beginning_of_month_timestamp, end_of_month_timestamp)
|
||||
points = points(user, beginning_of_month_timestamp, end_of_month_timestamp)
|
||||
next if points.empty?
|
||||
|
||||
stat = Stat.find_or_initialize_by(year:, month:, user:)
|
||||
|
|
@ -31,8 +31,9 @@ class CreateStats
|
|||
|
||||
private
|
||||
|
||||
def points(beginning_of_month_timestamp, end_of_month_timestamp)
|
||||
Point
|
||||
def points(user, beginning_of_month_timestamp, end_of_month_timestamp)
|
||||
user
|
||||
.tracked_points
|
||||
.without_raw_data
|
||||
.where(timestamp: beginning_of_month_timestamp..end_of_month_timestamp)
|
||||
.order(:timestamp)
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class GoogleMaps::SemanticHistoryParser
|
||||
attr_reader :import
|
||||
attr_reader :import, :user_id
|
||||
|
||||
def initialize(import)
|
||||
def initialize(import, user_id)
|
||||
@import = import
|
||||
@user_id = user_id
|
||||
end
|
||||
|
||||
def call
|
||||
|
|
@ -22,7 +23,8 @@ class GoogleMaps::SemanticHistoryParser
|
|||
raw_data: point_data[:raw_data],
|
||||
topic: 'Google Maps Timeline Export',
|
||||
tracker_id: 'google-maps-timeline-export',
|
||||
import_id: import.id
|
||||
import_id: import.id,
|
||||
user_id:
|
||||
)
|
||||
|
||||
points += 1
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class OwnTracks::ExportParser
|
||||
attr_reader :import, :json
|
||||
attr_reader :import, :json, :user_id
|
||||
|
||||
def initialize(import)
|
||||
def initialize(import, user_id)
|
||||
@import = import
|
||||
@json = import.raw_data
|
||||
@user_id = user_id
|
||||
end
|
||||
|
||||
def call
|
||||
|
|
@ -23,7 +24,8 @@ class OwnTracks::ExportParser
|
|||
raw_data: point_data[:raw_data],
|
||||
topic: point_data[:topic],
|
||||
tracker_id: point_data[:tracker_id],
|
||||
import_id: import.id
|
||||
import_id: import.id,
|
||||
user_id:
|
||||
)
|
||||
|
||||
points += 1
|
||||
|
|
|
|||
|
|
@ -2,8 +2,21 @@
|
|||
<p class='py-2'>Use this API key to authenticate your requests.</p>
|
||||
<code><%= current_user.api_key %></code>
|
||||
<p class='py-2'>
|
||||
<p>Docs: <%= link_to "API documentation", '/api-docs', class: 'underline hover:no-underline' %></p>
|
||||
|
||||
Usage example:
|
||||
<p><code><%= api_v1_points_url(api_key: current_user.api_key) %></code></p>
|
||||
|
||||
<div role="tablist" class="tabs tabs-boxed">
|
||||
<input type="radio" name="my_tabs_2" role="tab" class="tab" aria-label="OwnTracks" />
|
||||
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 rounded-box p-6">
|
||||
<p><code><%= api_v1_owntracks_points_url(api_key: current_user.api_key) %></code></p>
|
||||
</div>
|
||||
|
||||
<input type="radio" name="my_tabs_2" role="tab" class="tab" aria-label="Overland" checked />
|
||||
<div role="tabpanel" class="tab-content bg-base-100 border-base-300 rounded-box p-6">
|
||||
<p><code><%= api_v1_overland_batches_url(api_key: current_user.api_key) %></code></p>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
<p class='py-2'>
|
||||
<%= link_to "Generate new API key", generate_api_key_path, data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?", turbo_method: :post }, class: 'btn btn-primary' %>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
<div class="hero-content flex-col lg:flex-row-reverse w-full my-10">
|
||||
<div class="text-center lg:text-left">
|
||||
<h1 class="text-5xl font-bold">Edit your account!</h1>
|
||||
<%#= render 'devise/registrations/api_key' %>
|
||||
<%= render 'devise/registrations/api_key' %>
|
||||
</div>
|
||||
<div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100 px-5 py-5">
|
||||
<%= form_for(resource, as: resource_name, url: registration_path(resource_name), class: 'form-body', html: { method: :put }) do |f| %>
|
||||
|
|
|
|||
|
|
@ -22,11 +22,17 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<div role="alert" class="alert alert-warning">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /></svg>
|
||||
<span>Warning: Starting release 0.4.0 it's HIGHLY RECOMMENDED to switch from <code>/api/v1/points</code> to <code>/api/v1/owntracks/points</code> API endpoint. Please read more at <a href="https://github.com/Freika/dawarich/releases/tag/0.4.0" class='underline hover:no-underline'>0.4.0 release notes</a></span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="w-full"
|
||||
data-controller="maps"
|
||||
data-coordinates="<%= @coordinates %>"
|
||||
data-center="<%= MAP_CENTER %>">
|
||||
data-center="<%= MAP_CENTER %>"
|
||||
data-timezone="<%= Rails.configuration.time_zone %>">
|
||||
<div data-maps-target="container" class="h-[25rem] w-auto min-h-screen"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
<div class="dropdown">
|
||||
<div tabindex="0" role="button" class="btn m-1">Select year</div>
|
||||
<ul tabindex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52">
|
||||
<% Stat.years.each do |year| %>
|
||||
<% current_user.stats.years.each do |year| %>
|
||||
<li><%= link_to year, map_url(year_timespan(year).merge(year: year)) %></li>
|
||||
<% end %>
|
||||
</ul>
|
||||
|
|
@ -18,7 +18,7 @@
|
|||
<div class='grid grid-cols-3 gap-3'>
|
||||
<% (1..12).to_a.each_slice(3) do |months| %>
|
||||
<% months.each do |month_number| %>
|
||||
<% if past?(year, month_number) && points_exist?(year, month_number) %>
|
||||
<% if past?(year, month_number) && points_exist?(year, month_number, current_user) %>
|
||||
<%= 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>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
</h2>
|
||||
<div class='my-10'>
|
||||
<%= column_chart(
|
||||
Stat.year_distance(year),
|
||||
Stat.year_distance(year, current_user),
|
||||
height: '200px',
|
||||
suffix: ' km',
|
||||
xtitle: 'Days',
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@
|
|||
|
||||
<div class="stat text-center">
|
||||
<div class="stat-value text-success">
|
||||
<%= number_with_delimiter current_user.points.without_raw_data.count(:id) %>
|
||||
<%= number_with_delimiter current_user.tracked_points.without_raw_data.count(:id) %>
|
||||
</div>
|
||||
<div class="stat-title">Geopoints tracked</div>
|
||||
</div>
|
||||
|
|
@ -77,18 +77,18 @@
|
|||
</h2>
|
||||
<p>
|
||||
<% cache [current_user, 'year_distance_stat_in_km', year], skip_digest: true do %>
|
||||
<%= number_with_delimiter year_distance_stat_in_km(year) %>km
|
||||
<%= number_with_delimiter year_distance_stat_in_km(year, current_user) %>km
|
||||
<% end %>
|
||||
</p>
|
||||
<% if REVERSE_GEOCODING_ENABLED %>
|
||||
<div class="card-actions justify-end">
|
||||
<% cache [current_user, 'countries_and_cities_stat', year], skip_digest: true do %>
|
||||
<%= countries_and_cities_stat(year) %>
|
||||
<%= countries_and_cities_stat(year, current_user) %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= column_chart(
|
||||
Stat.year_distance(year),
|
||||
Stat.year_distance(year, current_user),
|
||||
height: '200px',
|
||||
suffix: ' km',
|
||||
xtitle: 'Days',
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ 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'
|
||||
|
|
@ -26,17 +27,21 @@ Rails.application.routes.draw do
|
|||
root to: 'home#index'
|
||||
devise_for :users
|
||||
|
||||
post 'settings/generate_api_key', to: 'devise/api_keys#create', as: :generate_api_key
|
||||
post 'settings/generate_api_key', to: 'settings#generate_api_key', as: :generate_api_key
|
||||
|
||||
get 'map', to: 'map#index'
|
||||
|
||||
namespace :api do
|
||||
namespace :v1 do
|
||||
resources :points
|
||||
resources :points, only: :create # TODO: Deprecate in 1.0
|
||||
|
||||
namespace :overland do
|
||||
resources :batches, only: :create
|
||||
end
|
||||
|
||||
namespace :owntracks do
|
||||
resources :points, only: :create
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
19
db/data/20240525110530_bind_existing_points_to_first_user.rb
Normal file
19
db/data/20240525110530_bind_existing_points_to_first_user.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class BindExistingPointsToFirstUser < ActiveRecord::Migration[7.1]
|
||||
def up
|
||||
user = User.first
|
||||
|
||||
return if user.blank?
|
||||
|
||||
points = Point.where(user_id: nil)
|
||||
|
||||
points.update_all(user_id: user.id)
|
||||
|
||||
Rails.logger.info "Bound #{points.count} points to user #{user.email}"
|
||||
end
|
||||
|
||||
def down
|
||||
raise ActiveRecord::IrreversibleMigration
|
||||
end
|
||||
end
|
||||
1
db/data_schema.rb
Normal file
1
db/data_schema.rb
Normal file
|
|
@ -0,0 +1 @@
|
|||
DataMigrate::Data.define(version: 20240525110530)
|
||||
7
db/migrate/20240525110244_add_user_id_to_points.rb
Normal file
7
db/migrate/20240525110244_add_user_id_to_points.rb
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUserIdToPoints < ActiveRecord::Migration[7.1]
|
||||
def change
|
||||
add_reference :points, :user, foreign_key: true
|
||||
end
|
||||
end
|
||||
5
db/schema.rb
generated
5
db/schema.rb
generated
|
|
@ -10,7 +10,7 @@
|
|||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_05_18_095848) do
|
||||
ActiveRecord::Schema[7.1].define(version: 2024_05_25_110244) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
|
|
@ -82,6 +82,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_18_095848) do
|
|||
t.string "country"
|
||||
t.datetime "created_at", null: false
|
||||
t.datetime "updated_at", null: false
|
||||
t.bigint "user_id"
|
||||
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"
|
||||
|
|
@ -92,6 +93,7 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_18_095848) do
|
|||
t.index ["latitude", "longitude"], name: "index_points_on_latitude_and_longitude"
|
||||
t.index ["timestamp"], name: "index_points_on_timestamp"
|
||||
t.index ["trigger"], name: "index_points_on_trigger"
|
||||
t.index ["user_id"], name: "index_points_on_user_id"
|
||||
end
|
||||
|
||||
create_table "stats", force: :cascade do |t|
|
||||
|
|
@ -125,5 +127,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_05_18_095848) do
|
|||
|
||||
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
|
||||
add_foreign_key "points", "users"
|
||||
add_foreign_key "stats", "users"
|
||||
end
|
||||
|
|
|
|||
|
|
@ -28,5 +28,9 @@ bundle exec rails db:create
|
|||
echo "PostgreSQL is ready. Running database migrations..."
|
||||
bundle exec rails db:prepare
|
||||
|
||||
# Run data migrations
|
||||
echo "Running DATA migrations..."
|
||||
bundle exec rake data:migrate
|
||||
|
||||
# run passed commands
|
||||
bundle exec ${@}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,7 @@ services:
|
|||
DATABASE_NAME: dawarich_development
|
||||
MIN_MINUTES_SPENT_IN_CITY: 60
|
||||
APPLICATION_HOST: localhost
|
||||
TIME_ZONE: UTC
|
||||
depends_on:
|
||||
- dawarich_db
|
||||
- dawarich_redis
|
||||
|
|
|
|||
|
|
@ -2,14 +2,21 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe Overland::BatchCreatingJob, type: :job do
|
||||
describe '#perform' do
|
||||
subject(:perform) { described_class.new.perform(json) }
|
||||
subject(:perform) { described_class.new.perform(json, user.id) }
|
||||
|
||||
let(:file_path) { 'spec/fixtures/files/overland/geodata.json' }
|
||||
let(:file) { File.open(file_path) }
|
||||
let(:json) { JSON.parse(file.read) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it 'creates a location' do
|
||||
expect { perform }.to change { Point.count }.by(1)
|
||||
end
|
||||
|
||||
it 'creates a point with the correct user_id' do
|
||||
perform
|
||||
|
||||
expect(Point.last.user_id).to eq(user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -2,14 +2,21 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe Owntracks::PointCreatingJob, type: :job do
|
||||
describe '#perform' do
|
||||
subject(:perform) { described_class.new.perform(point_params) }
|
||||
subject(:perform) { described_class.new.perform(point_params, user.id) }
|
||||
|
||||
let(:point_params) do
|
||||
{ lat: 1.0, lon: 1.0, tid: 'test', tst: Time.now.to_i, topic: 'iPhone 12 pro' }
|
||||
end
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it 'creates a point' do
|
||||
expect { perform }.to change { Point.count }.by(1)
|
||||
end
|
||||
|
||||
it 'creates a point with the correct user_id' do
|
||||
perform
|
||||
|
||||
expect(Point.last.user_id).to eq(user.id)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,8 +1,11 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Point, type: :model do
|
||||
describe 'associations' do
|
||||
it { is_expected.to belong_to(:import).optional }
|
||||
it { is_expected.to belong_to(:user).optional }
|
||||
end
|
||||
|
||||
describe 'validations' do
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ RSpec.describe Stat, type: :model do
|
|||
|
||||
describe 'methods' do
|
||||
let(:year) { 2021 }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
describe '.year_cities_and_countries' do
|
||||
subject { described_class.year_cities_and_countries(year) }
|
||||
subject { described_class.year_cities_and_countries(year, user) }
|
||||
|
||||
let(:timestamp) { DateTime.new(year, 1, 1, 0, 0, 0) }
|
||||
|
||||
|
|
@ -24,16 +25,16 @@ RSpec.describe Stat, type: :model do
|
|||
context 'when there are points' do
|
||||
let!(:points) do
|
||||
[
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp:),
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 10.minutes),
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 20.minutes),
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 30.minutes),
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 40.minutes),
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 50.minutes),
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 60.minutes),
|
||||
create(:point, city: 'Berlin', country: 'Germany', timestamp: timestamp + 70.minutes),
|
||||
create(:point, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 80.minutes),
|
||||
create(:point, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 90.minutes)
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp:),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 10.minutes),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 20.minutes),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 30.minutes),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 40.minutes),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 50.minutes),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 60.minutes),
|
||||
create(:point, user:, city: 'Berlin', country: 'Germany', timestamp: timestamp + 70.minutes),
|
||||
create(:point, user:, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 80.minutes),
|
||||
create(:point, user:, city: 'Brugges', country: 'Belgium', timestamp: timestamp + 90.minutes)
|
||||
]
|
||||
end
|
||||
|
||||
|
|
@ -86,8 +87,8 @@ RSpec.describe Stat, type: :model do
|
|||
|
||||
context 'when there are points' do
|
||||
let!(:points) do
|
||||
create(:point, latitude: 1, longitude: 1, timestamp: DateTime.new(year, 1, 1, 1))
|
||||
create(:point, latitude: 2, longitude: 2, timestamp: DateTime.new(year, 1, 1, 2))
|
||||
create(:point, user:, latitude: 1, longitude: 1, timestamp: DateTime.new(year, 1, 1, 1))
|
||||
create(:point, user:, latitude: 2, longitude: 2, timestamp: DateTime.new(year, 1, 1, 2))
|
||||
end
|
||||
|
||||
before { expected_distance[0][1] = 157.23 }
|
||||
|
|
@ -116,7 +117,7 @@ RSpec.describe Stat, type: :model do
|
|||
end
|
||||
|
||||
describe '#self.year_distance' do
|
||||
subject { described_class.year_distance(year) }
|
||||
subject { described_class.year_distance(year, user) }
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:expected_distance) do
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ RSpec.describe User, type: :model do
|
|||
it { is_expected.to have_many(:imports).dependent(:destroy) }
|
||||
it { is_expected.to have_many(:points).through(:imports) }
|
||||
it { is_expected.to have_many(:stats) }
|
||||
it { is_expected.to have_many(:tracked_points).class_name('Point').dependent(:destroy) }
|
||||
end
|
||||
|
||||
describe 'callbacks' do
|
||||
|
|
|
|||
|
|
@ -9,16 +9,28 @@ RSpec.describe 'Api::V1::Overland::Batches', type: :request do
|
|||
let(:json) { JSON.parse(file.read) }
|
||||
let(:params) { json }
|
||||
|
||||
it 'returns http success' do
|
||||
post '/api/v1/overland/batches', params: params
|
||||
context 'with invalid api key' do
|
||||
it 'returns http unauthorized' do
|
||||
post '/api/v1/overland/batches', params: params
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
it 'enqueues a job' do
|
||||
expect do
|
||||
post '/api/v1/overland/batches', params: params
|
||||
end.to have_enqueued_job(Overland::BatchCreatingJob)
|
||||
context 'with valid api key' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
it 'returns http success' do
|
||||
post "/api/v1/overland/batches?api_key=#{user.api_key}", params: params
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
end
|
||||
|
||||
it 'enqueues a job' do
|
||||
expect do
|
||||
post "/api/v1/overland/batches?api_key=#{user.api_key}", params: params
|
||||
end.to have_enqueued_job(Overland::BatchCreatingJob)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
36
spec/requests/api/v1/owntracks/points_spec.rb
Normal file
36
spec/requests/api/v1/owntracks/points_spec.rb
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Owntracks::Points', type: :request do
|
||||
describe 'POST /api/v1/owntracks/points' do
|
||||
context 'with valid params' do
|
||||
let(:params) do
|
||||
{ lat: 1.0, lon: 1.0, tid: 'test', tst: Time.current.to_i, topic: 'iPhone 12 pro' }
|
||||
end
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'with invalid api key' do
|
||||
it 'returns http unauthorized' do
|
||||
post api_v1_owntracks_points_path, params: params
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with valid api key' do
|
||||
it 'returns http success' do
|
||||
post api_v1_owntracks_points_path(api_key: user.api_key), params: params
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
end
|
||||
|
||||
it 'enqueues a job' do
|
||||
expect do
|
||||
post api_v1_owntracks_points_path(api_key: user.api_key), params: params
|
||||
end.to have_enqueued_job(Owntracks::PointCreatingJob)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -30,11 +30,12 @@ RSpec.describe '/points', type: :request do
|
|||
end
|
||||
|
||||
describe 'DELETE /bulk_destroy' do
|
||||
let(:point1) { create(:point) }
|
||||
let(:point2) { create(:point) }
|
||||
let(:user) { create(:user) }
|
||||
let(:point1) { create(:point, user:) }
|
||||
let(:point2) { create(:point, user:) }
|
||||
|
||||
before do
|
||||
sign_in create(:user)
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it 'destroys the selected points' do
|
||||
|
|
|
|||
|
|
@ -40,4 +40,32 @@ RSpec.describe 'Settings', type: :request do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'POST /generate_api_key' do
|
||||
context 'when user is not signed in' do
|
||||
it 'redirects to the sign in page' do
|
||||
post '/settings/generate_api_key'
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is signed in' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
it 'generates an API key for the user' do
|
||||
expect { post '/settings/generate_api_key' }.to change { user.reload.api_key }
|
||||
end
|
||||
|
||||
it 'redirects back' do
|
||||
post '/settings/generate_api_key'
|
||||
|
||||
expect(response).to redirect_to(root_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe CreateStats do
|
||||
|
|
@ -9,16 +11,15 @@ RSpec.describe CreateStats do
|
|||
|
||||
context 'when there are no points' do
|
||||
it 'does not create stats' do
|
||||
expect { create_stats }.not_to change { Stat.count }
|
||||
expect { create_stats }.not_to(change { Stat.count })
|
||||
end
|
||||
end
|
||||
|
||||
context 'when there are points' do
|
||||
let!(:import) { create(:import, user: user) }
|
||||
let!(:point_1) { create(:point, import: import, latitude: 0, longitude: 0) }
|
||||
let!(:point_2) { create(:point, import: import, latitude: 1, longitude: 2) }
|
||||
let!(:point_3) { create(:point, import: import, latitude: 3, longitude: 4) }
|
||||
|
||||
let!(:import) { create(:import, user:) }
|
||||
let!(:point1) { create(:point, user:, import:, latitude: 0, longitude: 0) }
|
||||
let!(:point2) { create(:point, user:, import:, latitude: 1, longitude: 2) }
|
||||
let!(:point3) { create(:point, user:, import:, latitude: 3, longitude: 4) }
|
||||
|
||||
it 'creates stats' do
|
||||
expect { create_stats }.to change { Stat.count }.by(1)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe OwnTracks::ExportParser do
|
||||
describe '#call' do
|
||||
subject(:parser) { described_class.new(import).call }
|
||||
subject(:parser) { described_class.new(import, user.id).call }
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:import) { create(:import, user:, name: 'owntracks_export.json') }
|
||||
|
|
|
|||
|
|
@ -72,12 +72,26 @@ describe 'Batches API', type: :request do
|
|||
}
|
||||
}
|
||||
|
||||
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
|
||||
|
||||
response '201', 'Batch of points created' do
|
||||
let(:file_path) { 'spec/fixtures/files/overland/geodata.json' }
|
||||
let(:file) { File.open(file_path) }
|
||||
let(:json) { JSON.parse(file.read) }
|
||||
let(:params) { json }
|
||||
let(:locations) { params['locations'] }
|
||||
let(:api_key) { create(:user).api_key }
|
||||
|
||||
run_test!
|
||||
end
|
||||
|
||||
response '401', 'Unauthorized' do
|
||||
let(:file_path) { 'spec/fixtures/files/overland/geodata.json' }
|
||||
let(:file) { File.open(file_path) }
|
||||
let(:json) { JSON.parse(file.read) }
|
||||
let(:params) { json }
|
||||
let(:locations) { params['locations'] }
|
||||
let(:api_key) { nil }
|
||||
|
||||
run_test!
|
||||
end
|
||||
|
|
|
|||
|
|
@ -9,10 +9,18 @@ paths:
|
|||
summary: Creates a batch of points
|
||||
tags:
|
||||
- Batches
|
||||
parameters: []
|
||||
parameters:
|
||||
- name: api_key
|
||||
in: query
|
||||
required: true
|
||||
description: API Key
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'201':
|
||||
description: Batch of points created
|
||||
'401':
|
||||
description: Unauthorized
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
|
|
@ -172,7 +180,7 @@ paths:
|
|||
lat: 52.502397
|
||||
lon: 13.356718
|
||||
tid: Swagger
|
||||
tst: 1716488929
|
||||
tst: 1716638918
|
||||
servers:
|
||||
- url: http://{defaultHost}
|
||||
variables:
|
||||
|
|
|
|||
Loading…
Reference in a new issue