mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 09:41:40 -05:00
Remove tracks api endpoint
This commit is contained in:
parent
565f92c463
commit
1468f1f9dc
14 changed files with 120 additions and 370 deletions
|
|
@ -1,39 +0,0 @@
|
||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class Api::V1::TracksController < ApiController
|
|
||||||
def index
|
|
||||||
start_time = parse_timestamp(params[:start_at])
|
|
||||||
end_time = parse_timestamp(params[:end_at])
|
|
||||||
|
|
||||||
# Find tracks that overlap with the date range
|
|
||||||
@tracks = current_api_user.tracks
|
|
||||||
.where('start_at <= ? AND end_at >= ?', end_time, start_time)
|
|
||||||
.order(:start_at)
|
|
||||||
|
|
||||||
render json: { tracks: @tracks }
|
|
||||||
end
|
|
||||||
|
|
||||||
def create
|
|
||||||
tracks_created = Tracks::CreateFromPoints.new(current_api_user).call
|
|
||||||
|
|
||||||
render json: {
|
|
||||||
message: "#{tracks_created} tracks created successfully",
|
|
||||||
tracks_created: tracks_created
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def parse_timestamp(timestamp_param)
|
|
||||||
return Time.current if timestamp_param.blank?
|
|
||||||
|
|
||||||
# Handle both Unix timestamps and ISO date strings
|
|
||||||
if timestamp_param.to_s.match?(/^\d+$/)
|
|
||||||
Time.zone.at(timestamp_param.to_i)
|
|
||||||
else
|
|
||||||
Time.zone.parse(timestamp_param)
|
|
||||||
end
|
|
||||||
rescue ArgumentError
|
|
||||||
Time.current
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -4,21 +4,65 @@ class MapController < ApplicationController
|
||||||
before_action :authenticate_user!
|
before_action :authenticate_user!
|
||||||
|
|
||||||
def index
|
def index
|
||||||
@points = points.where('timestamp >= ? AND timestamp <= ?', start_at, end_at)
|
@points = filtered_points
|
||||||
|
@coordinates = build_coordinates
|
||||||
@coordinates =
|
@tracks = build_tracks
|
||||||
@points.pluck(:lonlat, :battery, :altitude, :timestamp, :velocity, :id, :country, :track_id)
|
@distance = calculate_distance
|
||||||
.map { |lonlat, *rest| [lonlat.y, lonlat.x, *rest.map(&:to_s)] }
|
@start_at = parsed_start_at
|
||||||
@tracks = TrackSerializer.new(current_user, @coordinates).call
|
@end_at = parsed_end_at
|
||||||
@distance = distance
|
@years = years_range
|
||||||
@start_at = Time.zone.at(start_at)
|
@points_number = points_count
|
||||||
@end_at = Time.zone.at(end_at)
|
|
||||||
@years = (@start_at.year..@end_at.year).to_a
|
|
||||||
@points_number = @coordinates.count
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def filtered_points
|
||||||
|
points.where('timestamp >= ? AND timestamp <= ?', start_at, end_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_coordinates
|
||||||
|
@points.pluck(:lonlat, :battery, :altitude, :timestamp, :velocity, :id, :country, :track_id)
|
||||||
|
.map { |lonlat, *rest| [lonlat.y, lonlat.x, *rest.map(&:to_s)] }
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_track_ids
|
||||||
|
# Extract track IDs from coordinates (index 8: [lat, lng, battery, altitude, timestamp, velocity, id, country, track_id])
|
||||||
|
@coordinates.map { |coord| coord[8]&.to_i }.compact.uniq.reject(&:zero?)
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_tracks
|
||||||
|
track_ids = extract_track_ids
|
||||||
|
TrackSerializer.new(current_user, track_ids).call
|
||||||
|
end
|
||||||
|
|
||||||
|
def calculate_distance
|
||||||
|
distance = 0
|
||||||
|
|
||||||
|
@coordinates.each_cons(2) do
|
||||||
|
distance += Geocoder::Calculations.distance_between(
|
||||||
|
[_1[0], _1[1]], [_2[0], _2[1]], units: current_user.safe_settings.distance_unit.to_sym
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
distance.round(1)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parsed_start_at
|
||||||
|
Time.zone.at(start_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def parsed_end_at
|
||||||
|
Time.zone.at(end_at)
|
||||||
|
end
|
||||||
|
|
||||||
|
def years_range
|
||||||
|
(parsed_start_at.year..parsed_end_at.year).to_a
|
||||||
|
end
|
||||||
|
|
||||||
|
def points_count
|
||||||
|
@coordinates.count
|
||||||
|
end
|
||||||
|
|
||||||
def start_at
|
def start_at
|
||||||
return Time.zone.parse(params[:start_at]).to_i if params[:start_at].present?
|
return Time.zone.parse(params[:start_at]).to_i if params[:start_at].present?
|
||||||
return Time.zone.at(points.last.timestamp).beginning_of_day.to_i if points.any?
|
return Time.zone.at(points.last.timestamp).beginning_of_day.to_i if points.any?
|
||||||
|
|
@ -33,18 +77,6 @@ class MapController < ApplicationController
|
||||||
Time.zone.today.end_of_day.to_i
|
Time.zone.today.end_of_day.to_i
|
||||||
end
|
end
|
||||||
|
|
||||||
def distance
|
|
||||||
@distance ||= 0
|
|
||||||
|
|
||||||
@coordinates.each_cons(2) do
|
|
||||||
@distance += Geocoder::Calculations.distance_between(
|
|
||||||
[_1[0], _1[1]], [_2[0], _2[1]], units: current_user.safe_settings.distance_unit.to_sym
|
|
||||||
)
|
|
||||||
end
|
|
||||||
|
|
||||||
@distance.round(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
def points
|
def points
|
||||||
params[:import_id] ? points_from_import : points_from_user
|
params[:import_id] ? points_from_import : points_from_user
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
# Tracks Map Layer
|
|
||||||
|
|
||||||
This module provides functionality for rendering tracks as a separate layer on Leaflet maps in Dawarich.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Distinct visual styling** - Tracks use brown color to differentiate from blue polylines
|
|
||||||
- **Interactive hover/click** - Rich popups with track details including distance, duration, elevation
|
|
||||||
- **Consistent styling** - All tracks use the same brown color for easy identification
|
|
||||||
- **Layer management** - Integrates with Leaflet layer control
|
|
||||||
- **Performance optimized** - Uses canvas rendering and efficient event handling
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Basic Integration
|
|
||||||
|
|
||||||
The tracks layer is automatically integrated into the main maps controller:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
// Import the tracks module
|
|
||||||
import { createTracksLayer, updateTracksColors } from "../maps/tracks";
|
|
||||||
|
|
||||||
// Create tracks layer
|
|
||||||
const tracksLayer = createTracksLayer(tracksData, map, userSettings, distanceUnit);
|
|
||||||
|
|
||||||
// Add to map
|
|
||||||
tracksLayer.addTo(map);
|
|
||||||
```
|
|
||||||
|
|
||||||
### Styling
|
|
||||||
|
|
||||||
All tracks use a consistent brown color (#8B4513) to ensure they are easily distinguishable from the blue polylines used for regular routes.
|
|
||||||
|
|
||||||
### Track Data Format
|
|
||||||
|
|
||||||
Tracks expect data in this format:
|
|
||||||
|
|
||||||
```javascript
|
|
||||||
{
|
|
||||||
id: 123,
|
|
||||||
start_at: "2025-01-15T10:00:00Z",
|
|
||||||
end_at: "2025-01-15T11:30:00Z",
|
|
||||||
distance: 15000, // meters
|
|
||||||
duration: 5400, // seconds
|
|
||||||
avg_speed: 25.5, // km/h
|
|
||||||
elevation_gain: 200, // meters
|
|
||||||
elevation_loss: 150, // meters
|
|
||||||
elevation_max: 500, // meters
|
|
||||||
elevation_min: 300, // meters
|
|
||||||
original_path: "LINESTRING(-74.0060 40.7128, -74.0070 40.7130)", // PostGIS format
|
|
||||||
// OR
|
|
||||||
coordinates: [[40.7128, -74.0060], [40.7130, -74.0070]], // [lat, lng] array
|
|
||||||
// OR
|
|
||||||
path: [[40.7128, -74.0060], [40.7130, -74.0070]] // alternative coordinate format
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Coordinate Parsing
|
|
||||||
|
|
||||||
The module automatically handles different coordinate formats:
|
|
||||||
|
|
||||||
1. **Array format**: `track.coordinates` or `track.path` as `[[lat, lng], ...]`
|
|
||||||
2. **PostGIS LineString**: Parses `"LINESTRING(lng lat, lng lat, ...)"` format
|
|
||||||
3. **Fallback**: Creates simple line from start/end points if available
|
|
||||||
|
|
||||||
### API Integration
|
|
||||||
|
|
||||||
The tracks layer integrates with these API endpoints:
|
|
||||||
|
|
||||||
- **GET `/api/v1/tracks`** - Fetch existing tracks
|
|
||||||
- **POST `/api/v1/tracks`** - Trigger track generation from points
|
|
||||||
|
|
||||||
### Settings Integration
|
|
||||||
|
|
||||||
Track settings are integrated into the main map settings panel:
|
|
||||||
|
|
||||||
- **Show Tracks** - Toggle track layer visibility
|
|
||||||
- **Refresh Tracks** - Regenerate tracks from current points
|
|
||||||
|
|
||||||
### Layer Control
|
|
||||||
|
|
||||||
Tracks appear as "Tracks" in the Leaflet layer control, positioned above regular polylines with z-index 460.
|
|
||||||
|
|
||||||
## Visual Features
|
|
||||||
|
|
||||||
### Markers
|
|
||||||
|
|
||||||
- **Start marker**: 🚀 (rocket emoji)
|
|
||||||
- **End marker**: 🎯 (target emoji)
|
|
||||||
|
|
||||||
### Popup Content
|
|
||||||
|
|
||||||
Track popups display:
|
|
||||||
- Track ID
|
|
||||||
- Start/end timestamps
|
|
||||||
- Duration (formatted as days/hours/minutes)
|
|
||||||
- Total distance
|
|
||||||
- Average speed
|
|
||||||
- Elevation statistics (gain/loss/max/min)
|
|
||||||
|
|
||||||
### Interaction States
|
|
||||||
|
|
||||||
- **Default**: Brown polylines (weight: 4)
|
|
||||||
- **Hover**: Orange polylines (weight: 6)
|
|
||||||
- **Clicked**: Red polylines (weight: 8, persistent until clicked elsewhere)
|
|
||||||
|
|
||||||
## Performance Considerations
|
|
||||||
|
|
||||||
- Uses Leaflet canvas renderer for efficient rendering
|
|
||||||
- Custom pane (`tracksPane`) with z-index 460
|
|
||||||
- Efficient coordinate parsing with error handling
|
|
||||||
- Minimal DOM manipulation during interactions
|
|
||||||
|
|
||||||
## Error Handling
|
|
||||||
|
|
||||||
- Graceful handling of missing coordinate data
|
|
||||||
- Console warnings for unparseable track data
|
|
||||||
- Fallback to empty layer if tracks API unavailable
|
|
||||||
- Error messages for failed track generation
|
|
||||||
|
|
@ -68,7 +68,7 @@ module Calculateable
|
||||||
def convert_distance_to_meters(calculated_distance)
|
def convert_distance_to_meters(calculated_distance)
|
||||||
# For Track model - convert to meters for storage (Track expects distance in meters)
|
# For Track model - convert to meters for storage (Track expects distance in meters)
|
||||||
case user_distance_unit.to_s
|
case user_distance_unit.to_s
|
||||||
when 'miles', 'mi'
|
when 'mi'
|
||||||
(calculated_distance * 1609.344).round(2) # miles to meters
|
(calculated_distance * 1609.344).round(2) # miles to meters
|
||||||
else
|
else
|
||||||
(calculated_distance * 1000).round(2) # km to meters
|
(calculated_distance * 1000).round(2) # km to meters
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,12 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
class TrackSerializer
|
class TrackSerializer
|
||||||
def initialize(user, coordinates)
|
def initialize(user, track_ids)
|
||||||
@user = user
|
@user = user
|
||||||
@coordinates = coordinates
|
@track_ids = track_ids
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
# Extract track IDs from the coordinates that are already filtered by timeframe
|
|
||||||
track_ids = extract_track_ids_from_coordinates
|
|
||||||
return [] if track_ids.empty?
|
return [] if track_ids.empty?
|
||||||
|
|
||||||
# Show only tracks that have points in the selected timeframe
|
# Show only tracks that have points in the selected timeframe
|
||||||
|
|
@ -29,15 +27,7 @@ class TrackSerializer
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
attr_reader :user, :coordinates
|
attr_reader :user, :track_ids
|
||||||
|
|
||||||
def extract_track_ids_from_coordinates
|
|
||||||
# Extract track_id from coordinates (index 8: [lat, lng, battery, altitude, timestamp, velocity, id, country, track_id])
|
|
||||||
track_ids = coordinates.map { |coord| coord[8]&.to_i }.compact.uniq
|
|
||||||
track_ids.reject(&:zero?) # Remove any nil/zero track IDs
|
|
||||||
end
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def serialize_track_data(
|
def serialize_track_data(
|
||||||
id, start_at, end_at, distance, avg_speed, duration, elevation_gain,
|
id, start_at, end_at, distance, avg_speed, duration, elevation_gain,
|
||||||
|
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
# Tracks Services
|
|
||||||
|
|
||||||
This directory contains services for working with tracks generated from user points.
|
|
||||||
|
|
||||||
## Tracks::CreateFromPoints
|
|
||||||
|
|
||||||
This service takes all points for a user and creates tracks by splitting them based on the user's configured settings for distance and time thresholds.
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# Basic usage
|
|
||||||
user = User.find(123)
|
|
||||||
service = Tracks::CreateFromPoints.new(user)
|
|
||||||
tracks_created = service.call
|
|
||||||
|
|
||||||
puts "Created #{tracks_created} tracks for user #{user.email}"
|
|
||||||
```
|
|
||||||
|
|
||||||
### How it works
|
|
||||||
|
|
||||||
The service:
|
|
||||||
|
|
||||||
1. **Fetches all user points** ordered by timestamp
|
|
||||||
2. **Splits points into track segments** based on two thresholds:
|
|
||||||
- **Distance threshold**: `user.safe_settings.meters_between_routes` (default: 500 meters)
|
|
||||||
- **Time threshold**: `user.safe_settings.minutes_between_routes` (default: 30 minutes)
|
|
||||||
3. **Creates Track records** with calculated statistics:
|
|
||||||
- Distance (in meters)
|
|
||||||
- Duration (in seconds)
|
|
||||||
- Average speed (in km/h)
|
|
||||||
- Elevation statistics (gain, loss, min, max)
|
|
||||||
- PostGIS LineString path
|
|
||||||
4. **Associates points with tracks** by updating the `track_id` field
|
|
||||||
|
|
||||||
### Track Splitting Logic
|
|
||||||
|
|
||||||
A new track is created when either condition is met:
|
|
||||||
- **Time gap**: Time between consecutive points > time threshold
|
|
||||||
- **Distance gap**: Distance between consecutive points > distance threshold
|
|
||||||
|
|
||||||
### Example with custom settings
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# User with custom settings
|
|
||||||
user.update!(settings: {
|
|
||||||
'meters_between_routes' => 1000, # 1km distance threshold
|
|
||||||
'minutes_between_routes' => 60 # 1 hour time threshold
|
|
||||||
})
|
|
||||||
|
|
||||||
service = Tracks::CreateFromPoints.new(user)
|
|
||||||
service.call
|
|
||||||
```
|
|
||||||
|
|
||||||
### Background Job Usage
|
|
||||||
|
|
||||||
For large datasets, consider running in a background job:
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
class Tracks::CreateJob < ApplicationJob
|
|
||||||
queue_as :default
|
|
||||||
|
|
||||||
def perform(user_id)
|
|
||||||
user = User.find(user_id)
|
|
||||||
tracks_created = Tracks::CreateFromPoints.new(user).call
|
|
||||||
|
|
||||||
# Create notification for user
|
|
||||||
Notification.create!(
|
|
||||||
user: user,
|
|
||||||
title: 'Tracks Generated',
|
|
||||||
content: "Created #{tracks_created} tracks from your location data",
|
|
||||||
kind: :info
|
|
||||||
)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Enqueue the job
|
|
||||||
Tracks::CreateJob.perform_later(user.id)
|
|
||||||
```
|
|
||||||
|
|
||||||
### Console Usage
|
|
||||||
|
|
||||||
```ruby
|
|
||||||
# In Rails console
|
|
||||||
rails console
|
|
||||||
|
|
||||||
# Generate tracks for a specific user
|
|
||||||
user = User.find_by(email: 'user@example.com')
|
|
||||||
Tracks::CreateFromPoints.new(user).call
|
|
||||||
|
|
||||||
# Generate tracks for all users
|
|
||||||
User.find_each do |user|
|
|
||||||
tracks_created = Tracks::CreateFromPoints.new(user).call
|
|
||||||
puts "User #{user.id}: #{tracks_created} tracks created"
|
|
||||||
end
|
|
||||||
```
|
|
||||||
|
|
||||||
### Configuration
|
|
||||||
|
|
||||||
The service respects user settings:
|
|
||||||
|
|
||||||
- `meters_between_routes`: Maximum distance between points in the same track (meters)
|
|
||||||
- `minutes_between_routes`: Maximum time between points in the same track (minutes)
|
|
||||||
- `distance_unit`: Used for internal calculations (km/miles)
|
|
||||||
|
|
||||||
### Performance Considerations
|
|
||||||
|
|
||||||
- Uses database transactions for consistency
|
|
||||||
- Processes points with `find_each` to avoid loading all points into memory
|
|
||||||
- Destroys existing tracks before regenerating (use with caution)
|
|
||||||
- For users with many points, consider running as background job
|
|
||||||
|
|
||||||
### Track Statistics
|
|
||||||
|
|
||||||
Each track includes:
|
|
||||||
|
|
||||||
- **start_at/end_at**: First and last point timestamps
|
|
||||||
- **distance**: Total distance in meters (converted from user's preferred unit)
|
|
||||||
- **duration**: Total time in seconds
|
|
||||||
- **avg_speed**: Average speed in km/h
|
|
||||||
- **elevation_gain/loss**: Cumulative elevation changes
|
|
||||||
- **elevation_min/max**: Altitude range
|
|
||||||
- **original_path**: PostGIS LineString geometry
|
|
||||||
|
|
||||||
### Dependencies
|
|
||||||
|
|
||||||
- PostGIS for distance calculations and path geometry
|
|
||||||
- Existing `Tracks::BuildPath` service for creating LineString geometry
|
|
||||||
- User settings via `Users::SafeSettings`
|
|
||||||
- Point model with `Distanceable` concern
|
|
||||||
|
|
@ -6,5 +6,6 @@
|
||||||
- imports
|
- imports
|
||||||
- exports
|
- exports
|
||||||
- stats
|
- stats
|
||||||
|
- tracks
|
||||||
- reverse_geocoding
|
- reverse_geocoding
|
||||||
- visit_suggesting
|
- visit_suggesting
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ class CreateTracks < ActiveRecord::Migration[8.0]
|
||||||
t.datetime :end_at, null: false
|
t.datetime :end_at, null: false
|
||||||
t.references :user, null: false, foreign_key: true
|
t.references :user, null: false, foreign_key: true
|
||||||
t.line_string :original_path, null: false
|
t.line_string :original_path, null: false
|
||||||
t.float :distance
|
t.integer :distance
|
||||||
t.float :avg_speed
|
t.float :avg_speed
|
||||||
t.integer :duration
|
t.integer :duration
|
||||||
t.integer :elevation_gain
|
t.integer :elevation_gain
|
||||||
|
|
|
||||||
2
db/schema.rb
generated
2
db/schema.rb
generated
|
|
@ -223,7 +223,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_03_193657) do
|
||||||
t.datetime "end_at", null: false
|
t.datetime "end_at", null: false
|
||||||
t.bigint "user_id", null: false
|
t.bigint "user_id", null: false
|
||||||
t.geometry "original_path", limit: {srid: 0, type: "line_string"}, null: false
|
t.geometry "original_path", limit: {srid: 0, type: "line_string"}, null: false
|
||||||
t.float "distance"
|
t.integer "distance"
|
||||||
t.float "avg_speed"
|
t.float "avg_speed"
|
||||||
t.integer "duration"
|
t.integer "duration"
|
||||||
t.integer "elevation_gain"
|
t.integer "elevation_gain"
|
||||||
|
|
|
||||||
|
|
@ -6,15 +6,17 @@ RSpec.describe Tracks::CreateJob, type: :job do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
describe '#perform' do
|
describe '#perform' do
|
||||||
it 'calls the service and creates a notification' do
|
let(:service_instance) { instance_double(Tracks::CreateFromPoints) }
|
||||||
service_instance = instance_double(Tracks::CreateFromPoints)
|
let(:notification_service) { instance_double(Notifications::Create) }
|
||||||
|
|
||||||
|
before do
|
||||||
allow(Tracks::CreateFromPoints).to receive(:new).with(user).and_return(service_instance)
|
allow(Tracks::CreateFromPoints).to receive(:new).with(user).and_return(service_instance)
|
||||||
allow(service_instance).to receive(:call).and_return(3)
|
allow(service_instance).to receive(:call).and_return(3)
|
||||||
|
|
||||||
notification_service = instance_double(Notifications::Create)
|
|
||||||
allow(Notifications::Create).to receive(:new).and_return(notification_service)
|
allow(Notifications::Create).to receive(:new).and_return(notification_service)
|
||||||
allow(notification_service).to receive(:call)
|
allow(notification_service).to receive(:call)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls the service and creates a notification' do
|
||||||
described_class.new.perform(user.id)
|
described_class.new.perform(user.id)
|
||||||
|
|
||||||
expect(Tracks::CreateFromPoints).to have_received(:new).with(user)
|
expect(Tracks::CreateFromPoints).to have_received(:new).with(user)
|
||||||
|
|
@ -30,18 +32,17 @@ RSpec.describe Tracks::CreateJob, type: :job do
|
||||||
|
|
||||||
context 'when service raises an error' do
|
context 'when service raises an error' do
|
||||||
let(:error_message) { 'Something went wrong' }
|
let(:error_message) { 'Something went wrong' }
|
||||||
|
let(:service_instance) { instance_double(Tracks::CreateFromPoints) }
|
||||||
|
let(:notification_service) { instance_double(Notifications::Create) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
service_instance = instance_double(Tracks::CreateFromPoints)
|
|
||||||
allow(Tracks::CreateFromPoints).to receive(:new).with(user).and_return(service_instance)
|
allow(Tracks::CreateFromPoints).to receive(:new).with(user).and_return(service_instance)
|
||||||
allow(service_instance).to receive(:call).and_raise(StandardError, error_message)
|
allow(service_instance).to receive(:call).and_raise(StandardError, error_message)
|
||||||
|
allow(Notifications::Create).to receive(:new).and_return(notification_service)
|
||||||
|
allow(notification_service).to receive(:call)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'creates an error notification' do
|
it 'creates an error notification' do
|
||||||
notification_service = instance_double(Notifications::Create)
|
|
||||||
allow(Notifications::Create).to receive(:new).and_return(notification_service)
|
|
||||||
allow(notification_service).to receive(:call)
|
|
||||||
|
|
||||||
described_class.new.perform(user.id)
|
described_class.new.perform(user.id)
|
||||||
|
|
||||||
expect(Notifications::Create).to have_received(:new).with(
|
expect(Notifications::Create).to have_received(:new).with(
|
||||||
|
|
@ -74,7 +75,7 @@ RSpec.describe Tracks::CreateJob, type: :job do
|
||||||
|
|
||||||
describe 'queue' do
|
describe 'queue' do
|
||||||
it 'is queued on default queue' do
|
it 'is queued on default queue' do
|
||||||
expect(described_class.new.queue_name).to eq('default')
|
expect(described_class.new.queue_name).to eq('tracks')
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,17 @@ RSpec.describe Point, type: :model do
|
||||||
expect(point.country_id).to eq(country.id)
|
expect(point.country_id).to eq(country.id)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#recalculate_track' do
|
||||||
|
let(:point) { create(:point, track: track) }
|
||||||
|
let(:track) { create(:track) }
|
||||||
|
|
||||||
|
it 'recalculates the track' do
|
||||||
|
expect(track).to receive(:recalculate_path_and_distance!)
|
||||||
|
|
||||||
|
point.update(lonlat: 'POINT(-79.85581250721961 15.854775993302411)')
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'scopes' do
|
describe 'scopes' do
|
||||||
|
|
|
||||||
|
|
@ -54,7 +54,7 @@ RSpec.describe Track, type: :model do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#recalculate_distance!' do
|
describe '#recalculate_distance!' do
|
||||||
it 'recalculates and saves the distance' do
|
it 'recalculates and saves the distance' do
|
||||||
original_distance = track.distance
|
original_distance = track.distance
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
RSpec.describe "Api::V1::Tracks", type: :request do
|
|
||||||
describe "GET /index" do
|
|
||||||
pending "add some examples (or delete) #{__FILE__}"
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -6,11 +6,13 @@ RSpec.describe TrackSerializer do
|
||||||
describe '#call' do
|
describe '#call' do
|
||||||
let(:user) { create(:user) }
|
let(:user) { create(:user) }
|
||||||
|
|
||||||
context 'when serializing user tracks without date range restrictions' do
|
context 'when serializing user tracks with track IDs' do
|
||||||
subject(:serializer) { described_class.new(user, 1.year.ago.to_i, 1.year.from_now.to_i).call }
|
subject(:serializer) { described_class.new(user, track_ids).call }
|
||||||
|
|
||||||
let!(:track1) { create(:track, user: user, start_at: 2.hours.ago, end_at: 1.hour.ago) }
|
let!(:track1) { create(:track, user: user, start_at: 2.hours.ago, end_at: 1.hour.ago) }
|
||||||
let!(:track2) { create(:track, user: user, start_at: 4.hours.ago, end_at: 3.hours.ago) }
|
let!(:track2) { create(:track, user: user, start_at: 4.hours.ago, end_at: 3.hours.ago) }
|
||||||
|
let!(:track3) { create(:track, user: user, start_at: 6.hours.ago, end_at: 5.hours.ago) }
|
||||||
|
let(:track_ids) { [track1.id, track2.id] }
|
||||||
|
|
||||||
it 'returns an array of serialized tracks' do
|
it 'returns an array of serialized tracks' do
|
||||||
expect(serializer).to be_an(Array)
|
expect(serializer).to be_an(Array)
|
||||||
|
|
@ -20,6 +22,7 @@ RSpec.describe TrackSerializer do
|
||||||
it 'serializes each track correctly' do
|
it 'serializes each track correctly' do
|
||||||
serialized_ids = serializer.map { |track| track[:id] }
|
serialized_ids = serializer.map { |track| track[:id] }
|
||||||
expect(serialized_ids).to contain_exactly(track1.id, track2.id)
|
expect(serialized_ids).to contain_exactly(track1.id, track2.id)
|
||||||
|
expect(serialized_ids).not_to include(track3.id)
|
||||||
end
|
end
|
||||||
|
|
||||||
it 'formats timestamps as ISO8601 for all tracks' do
|
it 'formats timestamps as ISO8601 for all tracks' do
|
||||||
|
|
@ -49,41 +52,48 @@ RSpec.describe TrackSerializer do
|
||||||
expect(track[:elevation_min]).to be_a(Numeric)
|
expect(track[:elevation_min]).to be_a(Numeric)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
it 'orders tracks by start_at in ascending order' do
|
||||||
|
serialized_tracks = serializer
|
||||||
|
expect(serialized_tracks.first[:id]).to eq(track2.id) # Started 4 hours ago
|
||||||
|
expect(serialized_tracks.second[:id]).to eq(track1.id) # Started 2 hours ago
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when serializing user tracks with date range' do
|
context 'when track IDs belong to different users' do
|
||||||
subject(:serializer) { described_class.new(user, start_at.to_i, end_at.to_i).call }
|
subject(:serializer) { described_class.new(user, track_ids).call }
|
||||||
|
|
||||||
let(:start_at) { 6.hours.ago }
|
let(:other_user) { create(:user) }
|
||||||
let(:end_at) { 30.minutes.ago }
|
let!(:user_track) { create(:track, user: user) }
|
||||||
let!(:track_in_range) { create(:track, user: user, start_at: 2.hours.ago, end_at: 1.hour.ago) }
|
let!(:other_user_track) { create(:track, user: other_user) }
|
||||||
let!(:track_out_of_range) { create(:track, user: user, start_at: 10.hours.ago, end_at: 9.hours.ago) }
|
let(:track_ids) { [user_track.id, other_user_track.id] }
|
||||||
|
|
||||||
it 'returns an array of serialized tracks' do
|
it 'only returns tracks belonging to the specified user' do
|
||||||
expect(serializer).to be_an(Array)
|
|
||||||
expect(serializer.length).to eq(1)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'only includes tracks within the date range' do
|
|
||||||
serialized_ids = serializer.map { |track| track[:id] }
|
serialized_ids = serializer.map { |track| track[:id] }
|
||||||
expect(serialized_ids).to contain_exactly(track_in_range.id)
|
expect(serialized_ids).to contain_exactly(user_track.id)
|
||||||
expect(serialized_ids).not_to include(track_out_of_range.id)
|
expect(serialized_ids).not_to include(other_user_track.id)
|
||||||
end
|
|
||||||
|
|
||||||
it 'formats timestamps as ISO8601' do
|
|
||||||
serializer.each do |track|
|
|
||||||
expect(track[:start_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
|
||||||
expect(track[:end_at]).to match(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when user has no tracks' do
|
context 'when track IDs array is empty' do
|
||||||
subject(:serializer) { described_class.new(user, 1.day.ago.to_i, Time.current.to_i).call }
|
subject(:serializer) { described_class.new(user, []).call }
|
||||||
|
|
||||||
it 'returns an empty array' do
|
it 'returns an empty array' do
|
||||||
expect(serializer).to eq([])
|
expect(serializer).to eq([])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when track IDs contain non-existent IDs' do
|
||||||
|
subject(:serializer) { described_class.new(user, track_ids).call }
|
||||||
|
|
||||||
|
let!(:existing_track) { create(:track, user: user) }
|
||||||
|
let(:track_ids) { [existing_track.id, 999999] }
|
||||||
|
|
||||||
|
it 'only returns existing tracks' do
|
||||||
|
serialized_ids = serializer.map { |track| track[:id] }
|
||||||
|
expect(serialized_ids).to contain_exactly(existing_track.id)
|
||||||
|
expect(serializer.length).to eq(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue