mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Update tests and refactor some code
This commit is contained in:
parent
5ee3d43b10
commit
c86921a29b
14 changed files with 243 additions and 315 deletions
|
|
@ -9,7 +9,8 @@ TODO:
|
|||
- Specs for app/services/visits/merge_service.rb and rename it probably
|
||||
- Remove Stimulus controllers for visits on the Visits page
|
||||
- Revert changes to Visits page
|
||||
|
||||
- Decide on how to suggest visits for the past
|
||||
- Should visits be disabled for non-reverse-geocoded instances?
|
||||
|
||||
# 0.25.0 - 2025-03-08
|
||||
|
||||
|
|
|
|||
|
|
@ -14,17 +14,6 @@
|
|||
*= require_self
|
||||
*/
|
||||
|
||||
/* Leaflet map container styles */
|
||||
[data-controller="visits-map"] {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
[data-visits-map-target="container"] {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* Loading spinner animation */
|
||||
@keyframes spinner {
|
||||
to {
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export class VisitsManager {
|
|||
const SelectionControl = L.Control.extend({
|
||||
onAdd: (map) => {
|
||||
const button = L.DomUtil.create('button', 'leaflet-bar leaflet-control leaflet-control-custom');
|
||||
button.innerHTML = '<i class="fas fa-draw-polygon"></i>';
|
||||
button.innerHTML = '📌';
|
||||
button.title = 'Select Area';
|
||||
button.id = 'selection-tool-button';
|
||||
button.style.width = '48px';
|
||||
|
|
|
|||
|
|
@ -6,8 +6,24 @@ class DataMigrations::MigratePlacesLonlatJob < ApplicationJob
|
|||
def perform(user_id)
|
||||
user = User.find(user_id)
|
||||
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
user.places.update_all('lonlat = ST_SetSRID(ST_MakePoint(longitude, latitude), 4326)')
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
# Find all places with nil lonlat
|
||||
places_to_update = user.places.where(lonlat: nil)
|
||||
|
||||
# For each place, set the lonlat value based on longitude and latitude
|
||||
places_to_update.find_each do |place|
|
||||
next if place.longitude.nil? || place.latitude.nil?
|
||||
|
||||
# Set the lonlat to a PostGIS point with the proper SRID
|
||||
# rubocop:disable Rails/SkipsModelValidations
|
||||
place.update_column(:lonlat, "SRID=4326;POINT(#{place.longitude} #{place.latitude})")
|
||||
# rubocop:enable Rails/SkipsModelValidations
|
||||
end
|
||||
|
||||
# Double check if there are any remaining places without lonlat
|
||||
remaining = user.places.where(lonlat: nil)
|
||||
return unless remaining.exists?
|
||||
|
||||
# Log an error for these places
|
||||
Rails.logger.error("Places with ID #{remaining.pluck(:id).join(', ')} for user #{user.id} could not be updated with lonlat values")
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -22,8 +22,8 @@ module Visits
|
|||
return [] if points.empty?
|
||||
|
||||
potential_visits = Visits::Detector.new(points).detect_potential_visits
|
||||
merged_visits = Visits::Merger.new(points).merge_visits(potential_visits)
|
||||
grouped_visits = group_nearby_visits(merged_visits).flatten
|
||||
merged_visits = Visits::Merger.new(points).merge_visits(potential_visits)
|
||||
grouped_visits = group_nearby_visits(merged_visits).flatten
|
||||
|
||||
Visits::Creator.new(user).create_visits(grouped_visits)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -11,16 +11,8 @@ module Visits
|
|||
def call
|
||||
# If the start date is in the future or equal to the end date,
|
||||
# handle as a special case extending to the end of the start's year
|
||||
if start_at >= end_at
|
||||
year_end = start_at.end_of_year
|
||||
return [start_at...year_end]
|
||||
end
|
||||
|
||||
# Special case for dates within the same year
|
||||
if start_at.year == end_at.year
|
||||
year_end = start_at.end_of_year
|
||||
return [start_at...year_end]
|
||||
end
|
||||
# or if the start and end are in the same year, return the year chunk
|
||||
return [start_at..start_at.end_of_year] if start_in_future? || same_year?
|
||||
|
||||
# First chunk: from start_at to end of that year
|
||||
first_end = start_at.end_of_year
|
||||
|
|
@ -43,5 +35,13 @@ module Visits
|
|||
private
|
||||
|
||||
attr_reader :start_at, :end_at, :time_chunks
|
||||
|
||||
def start_in_future?
|
||||
start_at >= end_at
|
||||
end
|
||||
|
||||
def same_year?
|
||||
start_at.year == end_at.year
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@
|
|||
<link href="https://cdn.jsdelivr.net/npm/daisyui@4.12.14/dist/full.css" rel="stylesheet" type="text/css">
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin="" />
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" integrity="sha512-DTOQO9RWCH3ppGqcWaEA1BIZOC6xxalwEsw9c2QQeAIftl+Vegovlnee1c9QX4TctnWMn13TZye+giMm8e2LwA==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
||||
|
||||
<%= stylesheet_link_tag "tailwind", "inter-font", "data-turbo-track": "reload" %>
|
||||
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,4 @@
|
|||
<div class="group relative timeline-box"
|
||||
data-action="mouseenter->visits-map#highlightVisit mouseleave->visits-map#unhighlightVisit"
|
||||
data-visit-id="<%= visit.id %>"
|
||||
data-center-lat="<%= visit.center[0] %>"
|
||||
data-center-lon="<%= visit.center[1] %>">
|
||||
<div class="group relative timeline-box">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<%= render 'visits/name', visit: visit %>
|
||||
|
|
|
|||
|
|
@ -1,103 +1,89 @@
|
|||
<% content_for :title, "Visits" %>
|
||||
|
||||
<div class="w-full h-screen flex flex-col">
|
||||
<%# Top navigation tabs %>
|
||||
<div class="w-full">
|
||||
<div role="tablist" class="tabs tabs-lifted tabs-lg">
|
||||
<%= link_to 'Visits', visits_path(status: :confirmed), role: 'tab', class: "tab font-bold text-xl #{active_visit_places_tab?('visits')}" %>
|
||||
<%= link_to 'Places', places_path, role: 'tab', class: "tab font-bold text-xl #{active_visit_places_tab?('places')}" %>
|
||||
<div class="w-full my-5">
|
||||
<div role="tablist" class="tabs tabs-lifted tabs-lg">
|
||||
<%= link_to 'Visits', visits_path(status: :confirmed), role: 'tab', class: "tab font-bold text-xl #{active_visit_places_tab?('visits')}" %>
|
||||
<%= link_to 'Places', places_path, role: 'tab', class: "tab font-bold text-xl #{active_visit_places_tab?('places')}" %>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-between">
|
||||
<div role="tablist" class="tabs tabs-boxed">
|
||||
<%= link_to 'Confirmed', visits_path(status: :confirmed), role: 'tab',
|
||||
class: "tab #{active_tab?(visits_path(status: :confirmed))}" %>
|
||||
<%= link_to visits_path(status: :suggested), role: 'tab',
|
||||
class: "tab #{active_tab?(visits_path(status: :suggested))}" do %>
|
||||
Suggested
|
||||
<% if @suggested_visits_count.positive? %>
|
||||
<span class="badge badge-sm badge-primary mx-1"><%= @suggested_visits_count %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= link_to 'Declined', visits_path(status: :declined), role: 'tab',
|
||||
class: "tab #{active_tab?(visits_path(status: :declined))}" %>
|
||||
</div>
|
||||
<div class="flex items-center">
|
||||
<span class="mr-2">Order by:</span>
|
||||
<%= link_to 'Newest', visits_path(order_by: :desc, status: params[:status]), class: 'btn btn-xs btn-primary mx-1' %>
|
||||
<%= link_to 'Oldest', visits_path(order_by: :asc, status: params[:status]), class: 'btn btn-xs btn-primary mx-1' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# Main content area with map and side panel %>
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
<%# Map container %>
|
||||
<div class="w-2/3 h-full relative" data-controller="visits-map">
|
||||
<div data-visits-map-target="container" class="w-full h-full"></div>
|
||||
<div role="alert" class="alert mt-5">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="stroke-info h-6 w-6 shrink-0">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Visits suggestion feature is currently in beta stage. Expect bugs and problems and don't hesitate to report them to <a href='https://github.com/Freika/dawarich/issues' class='link'>Github Issues</a>.</span>
|
||||
</div>
|
||||
|
||||
<% if @visits.empty? %>
|
||||
<div class="hero min-h-80 bg-base-200">
|
||||
<div class="hero-content text-center">
|
||||
<div class="max-w-md">
|
||||
<h1 class="text-5xl font-bold">Hello there!</h1>
|
||||
<p class="py-6">
|
||||
Here you'll find your <%= params[:status] %> visits, but now there are none. Create some areas on your map and pretty soon you'll see visit suggestions on this page!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# Side panel %>
|
||||
<div class="w-1/3 h-full flex flex-col bg-base-200 p-4">
|
||||
<%# Visit filters %>
|
||||
<div class="flex flex-col gap-4 mb-4">
|
||||
<div role="tablist" class="tabs tabs-boxed">
|
||||
<%= link_to 'Confirmed', visits_path(status: :confirmed), role: 'tab',
|
||||
class: "tab #{active_tab?(visits_path(status: :confirmed))}" %>
|
||||
<%= link_to visits_path(status: :suggested), role: 'tab',
|
||||
class: "tab #{active_tab?(visits_path(status: :suggested))}" do %>
|
||||
Suggested
|
||||
<% if @suggested_visits_count.positive? %>
|
||||
<span class="badge badge-sm badge-primary mx-1"><%= @suggested_visits_count %></span>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<%= link_to 'Declined', visits_path(status: :declined), role: 'tab',
|
||||
class: "tab #{active_tab?(visits_path(status: :declined))}" %>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end">
|
||||
<span class="mr-2">Order by:</span>
|
||||
<%= link_to 'Newest', visits_path(order_by: :desc, status: params[:status]), class: 'btn btn-xs btn-primary mx-1' %>
|
||||
<%= link_to 'Oldest', visits_path(order_by: :asc, status: params[:status]), class: 'btn btn-xs btn-primary mx-1' %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# Beta notice %>
|
||||
<div role="alert" class="alert mb-4">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-info h-6 w-6 shrink-0">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Visits suggestion feature is currently in beta stage. Expect bugs and problems and don't hesitate to report them to <a href='https://github.com/Freika/dawarich/issues' class='link'>Github Issues</a>.</span>
|
||||
</div>
|
||||
|
||||
<%# Visits list %>
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<% if @visits.empty? %>
|
||||
<div class="text-center py-8">
|
||||
<h2 class="text-2xl font-bold mb-4">No visits found</h2>
|
||||
<p class="text-base-content/70">
|
||||
Here you'll find your <%= params[:status] %> visits, but now there are none. Create some areas on your map and pretty soon you'll see visit suggestions on this page!
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="space-y-4">
|
||||
<% @visits.each do |visit| %>
|
||||
<div class="card bg-base-100 shadow-xl"
|
||||
data-action="mouseenter->visits-map#highlightVisit mouseleave->visits-map#unhighlightVisit"
|
||||
data-visit-id="<%= visit.id %>"
|
||||
data-center-lat="<%= visit.center[0] %>"
|
||||
data-center-lon="<%= visit.center[1] %>">
|
||||
<div class="card-body p-4">
|
||||
<%# Visit name %>
|
||||
<div class="flex items-center justify-between">
|
||||
<%= render 'visits/name', visit: visit %>
|
||||
<div class="badge <%= visit.confirmed? ? 'badge-success' : 'badge-warning' %>">
|
||||
<%= visit.status %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%# Visit time and duration %>
|
||||
<div class="text-sm text-base-content/70">
|
||||
<div><%= "#{visit.started_at.strftime('%H:%M')} - #{visit.ended_at.strftime('%H:%M')}" %></div>
|
||||
<div>Duration: <%= (visit.duration / 60.0).round(1) %> hours</div>
|
||||
</div>
|
||||
|
||||
<%# Action buttons %>
|
||||
<div class="card-actions justify-end mt-2">
|
||||
<%= render 'visits/buttons', visit: visit %>
|
||||
<label for="visit_details_popup_<%= visit.id %>" class='btn btn-xs btn-info'>Map</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<%= render 'visits/modal', visit: visit %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%# Pagination %>
|
||||
<div class="mt-4 flex justify-center">
|
||||
<% else %>
|
||||
<div class="flex justify-center my-5">
|
||||
<div class='flex'>
|
||||
<%= paginate @visits %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="timeline timeline-snap-icon max-md:timeline-compact timeline-vertical">
|
||||
<% @visits.each do |visit| %>
|
||||
<li>
|
||||
<div class="timeline-middle">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 20 20"
|
||||
fill="<%= visit.confirmed? ? 'green' : 'currentColor' %>"
|
||||
class="h-5 w-5">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.857-9.809a.75.75 0 00-1.214-.882l-3.483 4.79-1.88-1.88a.75.75 0 10-1.06 1.061l2.5 2.5a.75.75 0 001.137-.089l4-5.5z"
|
||||
clip-rule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="timeline-start md:text-end">
|
||||
<time class="font-mono italic"><%= visit.started_at.strftime('%A, %d %B %Y') %></time>
|
||||
</div>
|
||||
<div class="timeline-end md:text-end">
|
||||
<%= render partial: 'visit', locals: { visit: visit } %>
|
||||
</div>
|
||||
<hr />
|
||||
</li>
|
||||
<% end %>
|
||||
</ul>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ FactoryBot.define do
|
|||
name { 'MyString' }
|
||||
latitude { 54.2905245 }
|
||||
longitude { 13.0948638 }
|
||||
lonlat { "POINT(#{longitude} #{latitude})" }
|
||||
lonlat { "SRID=4326;POINT(#{longitude} #{latitude})" }
|
||||
|
||||
trait :with_geodata do
|
||||
geodata do
|
||||
|
|
@ -39,5 +39,14 @@ FactoryBot.define do
|
|||
}
|
||||
end
|
||||
end
|
||||
|
||||
# Special trait for testing with nil lonlat
|
||||
trait :without_lonlat do
|
||||
# Skip validation to create an invalid record for testing
|
||||
to_create { |instance| instance.save(validate: false) }
|
||||
after(:build) do |place|
|
||||
place.lonlat = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,5 +1,82 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe DataMigrations::MigratePlacesLonlatJob, type: :job do
|
||||
pending "add some examples to (or delete) #{__FILE__}"
|
||||
describe '#perform' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
context 'when places exist for the user' do
|
||||
let!(:place1) { create(:place, :without_lonlat, longitude: 10.0, latitude: 20.0) }
|
||||
let!(:place2) { create(:place, :without_lonlat, longitude: -73.935242, latitude: 40.730610) }
|
||||
let!(:other_place) { create(:place, :without_lonlat, longitude: 15.0, latitude: 25.0) }
|
||||
|
||||
# Create visits to associate places with users
|
||||
let!(:visit1) { create(:visit, user: user, place: place1) }
|
||||
let!(:visit2) { create(:visit, user: user, place: place2) }
|
||||
let!(:other_visit) { create(:visit, place: other_place) } # associated with a different user
|
||||
|
||||
it 'updates lonlat field for all places belonging to the user' do
|
||||
# Force a reload to ensure we have the initial state
|
||||
place1.reload
|
||||
place2.reload
|
||||
|
||||
# Both places should have nil lonlat initially
|
||||
expect(place1.lonlat).to be_nil
|
||||
expect(place2.lonlat).to be_nil
|
||||
|
||||
# Run the job
|
||||
described_class.perform_now(user.id)
|
||||
|
||||
# Reload to get updated state
|
||||
place1.reload
|
||||
place2.reload
|
||||
other_place.reload
|
||||
|
||||
# Check that lonlat is now set correctly
|
||||
expect(place1.lonlat).not_to be_nil
|
||||
expect(place2.lonlat).not_to be_nil
|
||||
|
||||
# The other user's place should still have nil lonlat
|
||||
expect(other_place.lonlat).to be_nil
|
||||
|
||||
# Verify the coordinates
|
||||
expect(place1.lonlat.x).to eq(10.0) # longitude
|
||||
expect(place1.lonlat.y).to eq(20.0) # latitude
|
||||
|
||||
expect(place2.lonlat.x).to eq(-73.935242) # longitude
|
||||
expect(place2.lonlat.y).to eq(40.730610) # latitude
|
||||
end
|
||||
|
||||
it 'sets the correct SRID (4326) on the geometry' do
|
||||
described_class.perform_now(user.id)
|
||||
place1.reload
|
||||
|
||||
# SRID should be 4326 (WGS84)
|
||||
expect(place1.lonlat.srid).to eq(4326)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when no places exist for the user' do
|
||||
it 'completes successfully without errors' do
|
||||
expect do
|
||||
described_class.perform_now(user.id)
|
||||
end.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user does not exist' do
|
||||
it 'raises ActiveRecord::RecordNotFound' do
|
||||
expect do
|
||||
described_class.perform_now(-1)
|
||||
end.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'queue' do
|
||||
it 'uses the default queue' do
|
||||
expect(described_class.queue_name).to eq('default')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -72,20 +72,19 @@ RSpec.describe VisitSuggestingJob, type: :job do
|
|||
end
|
||||
|
||||
context 'with string dates' do
|
||||
it 'handles string date parameters correctly' do
|
||||
let(:string_start) { start_at.to_s }
|
||||
let(:string_end) { end_at.to_s }
|
||||
let(:parsed_start) { start_at.to_datetime }
|
||||
let(:parsed_end) { end_at.to_datetime }
|
||||
|
||||
before do
|
||||
allow(Visits::Suggest).to receive(:new).and_call_original
|
||||
allow_any_instance_of(Visits::Suggest).to receive(:call)
|
||||
|
||||
string_start = start_at.to_s
|
||||
string_end = end_at.to_s
|
||||
|
||||
# We'll mock the Time.zone.parse method to return predictable values
|
||||
parsed_start = start_at.to_datetime
|
||||
parsed_end = end_at.to_datetime
|
||||
|
||||
allow(Time.zone).to receive(:parse).with(string_start).and_return(parsed_start)
|
||||
allow(Time.zone).to receive(:parse).with(string_end).and_return(parsed_end)
|
||||
end
|
||||
|
||||
it 'handles string date parameters correctly' do
|
||||
# At minimum we expect one call to Suggest
|
||||
expect(Visits::Suggest).to receive(:new).at_least(:once).and_call_original
|
||||
|
||||
|
|
@ -100,6 +99,7 @@ RSpec.describe VisitSuggestingJob, type: :job do
|
|||
context 'when user is inactive' do
|
||||
before do
|
||||
user.update(status: :inactive)
|
||||
|
||||
allow(Visits::Suggest).to receive(:new).and_call_original
|
||||
allow_any_instance_of(Visits::Suggest).to receive(:call)
|
||||
end
|
||||
|
|
@ -107,6 +107,7 @@ RSpec.describe VisitSuggestingJob, type: :job do
|
|||
it 'still processes the job for the specified user' do
|
||||
# The job doesn't check for user active status, it just processes whatever user is passed
|
||||
expect(Visits::Suggest).to receive(:new).at_least(:once).and_call_original
|
||||
|
||||
subject
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,53 +5,46 @@ require 'rails_helper'
|
|||
RSpec.describe Api::PlaceSerializer do
|
||||
describe '#call' do
|
||||
let(:place) do
|
||||
instance_double(
|
||||
Place,
|
||||
id: 123,
|
||||
create(
|
||||
:place,
|
||||
:with_geodata,
|
||||
name: 'Central Park',
|
||||
lon: -73.9665,
|
||||
lat: 40.7812,
|
||||
longitude: -73.9665,
|
||||
latitude: 40.7812,
|
||||
lonlat: 'SRID=4326;POINT(-73.9665 40.7812)',
|
||||
city: 'New York',
|
||||
country: 'United States',
|
||||
source: 'osm',
|
||||
geodata: { 'amenity' => 'park', 'leisure' => 'park' },
|
||||
reverse_geocoded_at: Time.zone.parse('2023-01-15T12:00:00Z')
|
||||
source: 'photon',
|
||||
geodata: { 'amenity' => 'park', 'leisure' => 'park' }, reverse_geocoded_at: Time.zone.parse('2023-01-15T12:00:00Z')
|
||||
)
|
||||
end
|
||||
|
||||
subject(:serializer) { described_class.new(place) }
|
||||
|
||||
it 'initializes with a place object' do
|
||||
expect(serializer.instance_variable_get(:@place)).to eq(place)
|
||||
end
|
||||
|
||||
it 'serializes a place into a hash with all attributes' do
|
||||
result = serializer.call
|
||||
|
||||
expect(result).to be_a(Hash)
|
||||
expect(result[:id]).to eq(123)
|
||||
expect(result[:id]).to eq(place.id)
|
||||
expect(result[:name]).to eq('Central Park')
|
||||
expect(result[:longitude]).to eq(-73.9665)
|
||||
expect(result[:latitude]).to eq(40.7812)
|
||||
expect(result[:city]).to eq('New York')
|
||||
expect(result[:country]).to eq('United States')
|
||||
expect(result[:source]).to eq('osm')
|
||||
expect(result[:source]).to eq('photon')
|
||||
expect(result[:geodata]).to eq({ 'amenity' => 'park', 'leisure' => 'park' })
|
||||
expect(result[:reverse_geocoded_at]).to eq(Time.zone.parse('2023-01-15T12:00:00Z'))
|
||||
end
|
||||
|
||||
context 'with nil values' do
|
||||
let(:place_with_nils) do
|
||||
instance_double(
|
||||
Place,
|
||||
id: 456,
|
||||
create(
|
||||
:place,
|
||||
name: 'Unknown Place',
|
||||
lon: nil,
|
||||
lat: nil,
|
||||
city: nil,
|
||||
country: nil,
|
||||
source: nil,
|
||||
geodata: nil,
|
||||
geodata: {},
|
||||
reverse_geocoded_at: nil
|
||||
)
|
||||
end
|
||||
|
|
@ -61,35 +54,14 @@ RSpec.describe Api::PlaceSerializer do
|
|||
it 'handles nil values correctly' do
|
||||
result = serializer_with_nils.call
|
||||
|
||||
expect(result[:id]).to eq(456)
|
||||
expect(result[:id]).to eq(place_with_nils.id)
|
||||
expect(result[:name]).to eq('Unknown Place')
|
||||
expect(result[:longitude]).to be_nil
|
||||
expect(result[:latitude]).to be_nil
|
||||
expect(result[:city]).to be_nil
|
||||
expect(result[:country]).to be_nil
|
||||
expect(result[:source]).to be_nil
|
||||
expect(result[:geodata]).to be_nil
|
||||
expect(result[:geodata]).to eq({})
|
||||
expect(result[:reverse_geocoded_at]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with actual Place model', type: :model do
|
||||
let(:real_place) { create(:place) }
|
||||
subject(:real_serializer) { described_class.new(real_place) }
|
||||
|
||||
it 'serializes a real place model correctly' do
|
||||
result = real_serializer.call
|
||||
|
||||
expect(result[:id]).to eq(real_place.id)
|
||||
expect(result[:name]).to eq(real_place.name)
|
||||
expect(result[:longitude]).to eq(real_place.lon)
|
||||
expect(result[:latitude]).to eq(real_place.lat)
|
||||
expect(result[:city]).to eq(real_place.city)
|
||||
expect(result[:country]).to eq(real_place.country)
|
||||
expect(result[:source]).to eq(real_place.source)
|
||||
expect(result[:geodata]).to eq(real_place.geodata)
|
||||
expect(result[:reverse_geocoded_at]).to eq(real_place.reverse_geocoded_at)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -4,145 +4,27 @@ require 'rails_helper'
|
|||
|
||||
RSpec.describe Api::VisitSerializer do
|
||||
describe '#call' do
|
||||
let(:place) do
|
||||
instance_double(
|
||||
Place,
|
||||
id: 123,
|
||||
lat: 40.7812,
|
||||
lon: -73.9665
|
||||
)
|
||||
end
|
||||
|
||||
let(:area) do
|
||||
instance_double(
|
||||
Area,
|
||||
id: 456,
|
||||
latitude: 41.9028,
|
||||
longitude: -87.6350
|
||||
)
|
||||
end
|
||||
|
||||
let(:visit) do
|
||||
instance_double(
|
||||
Visit,
|
||||
id: 789,
|
||||
area_id: area.id,
|
||||
user_id: 101,
|
||||
started_at: Time.zone.parse('2023-01-15T10:00:00Z'),
|
||||
ended_at: Time.zone.parse('2023-01-15T12:00:00Z'),
|
||||
duration: 120, # 2 hours in minutes
|
||||
name: 'Central Park Visit',
|
||||
status: 'confirmed',
|
||||
place: place,
|
||||
area: area,
|
||||
place_id: place.id
|
||||
)
|
||||
end
|
||||
let(:place) { create(:place) }
|
||||
let(:area) { create(:area) }
|
||||
let(:visit) { create(:visit, place: place, area: area) }
|
||||
|
||||
subject(:serializer) { described_class.new(visit) }
|
||||
|
||||
context 'when a visit has both place and area' do
|
||||
it 'serializes the visit with place coordinates' do
|
||||
result = serializer.call
|
||||
it 'serializes a real visit model correctly' do
|
||||
result = serializer.call
|
||||
|
||||
expect(result).to be_a(Hash)
|
||||
expect(result[:id]).to eq(789)
|
||||
expect(result[:area_id]).to eq(456)
|
||||
expect(result[:user_id]).to eq(101)
|
||||
expect(result[:started_at]).to eq(Time.zone.parse('2023-01-15T10:00:00Z'))
|
||||
expect(result[:ended_at]).to eq(Time.zone.parse('2023-01-15T12:00:00Z'))
|
||||
expect(result[:duration]).to eq(120)
|
||||
expect(result[:name]).to eq('Central Park Visit')
|
||||
expect(result[:status]).to eq('confirmed')
|
||||
expect(result[:id]).to eq(visit.id)
|
||||
expect(result[:area_id]).to eq(visit.area_id)
|
||||
expect(result[:user_id]).to eq(visit.user_id)
|
||||
expect(result[:started_at]).to eq(visit.started_at)
|
||||
expect(result[:ended_at]).to eq(visit.ended_at)
|
||||
expect(result[:duration]).to eq(visit.duration)
|
||||
expect(result[:name]).to eq(visit.name)
|
||||
expect(result[:status]).to eq(visit.status)
|
||||
|
||||
# Place should use place coordinates
|
||||
expect(result[:place][:id]).to eq(123)
|
||||
expect(result[:place][:latitude]).to eq(40.7812)
|
||||
expect(result[:place][:longitude]).to eq(-73.9665)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a visit has area but no place' do
|
||||
let(:visit_without_place) do
|
||||
instance_double(
|
||||
Visit,
|
||||
id: 789,
|
||||
area_id: area.id,
|
||||
user_id: 101,
|
||||
started_at: Time.zone.parse('2023-01-15T10:00:00Z'),
|
||||
ended_at: Time.zone.parse('2023-01-15T12:00:00Z'),
|
||||
duration: 120,
|
||||
name: 'Chicago Visit',
|
||||
status: 'suggested',
|
||||
place: nil,
|
||||
area: area,
|
||||
place_id: nil
|
||||
)
|
||||
end
|
||||
|
||||
subject(:serializer_without_place) { described_class.new(visit_without_place) }
|
||||
|
||||
it 'falls back to area coordinates' do
|
||||
result = serializer_without_place.call
|
||||
|
||||
expect(result[:place][:id]).to be_nil
|
||||
expect(result[:place][:latitude]).to eq(41.9028)
|
||||
expect(result[:place][:longitude]).to eq(-87.6350)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a visit has neither place nor area' do
|
||||
let(:visit_without_location) do
|
||||
instance_double(
|
||||
Visit,
|
||||
id: 789,
|
||||
area_id: nil,
|
||||
user_id: 101,
|
||||
started_at: Time.zone.parse('2023-01-15T10:00:00Z'),
|
||||
ended_at: Time.zone.parse('2023-01-15T12:00:00Z'),
|
||||
duration: 120,
|
||||
name: 'Unknown Location Visit',
|
||||
status: 'declined',
|
||||
place: nil,
|
||||
area: nil,
|
||||
place_id: nil
|
||||
)
|
||||
end
|
||||
|
||||
subject(:serializer_without_location) { described_class.new(visit_without_location) }
|
||||
|
||||
it 'returns nil for location coordinates' do
|
||||
result = serializer_without_location.call
|
||||
|
||||
expect(result[:place][:id]).to be_nil
|
||||
expect(result[:place][:latitude]).to be_nil
|
||||
expect(result[:place][:longitude]).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with actual Visit model', type: :model do
|
||||
let(:real_place) { create(:place) }
|
||||
let(:real_area) { create(:area) }
|
||||
let(:real_visit) { create(:visit, place: real_place, area: real_area) }
|
||||
|
||||
subject(:real_serializer) { described_class.new(real_visit) }
|
||||
|
||||
it 'serializes a real visit model correctly' do
|
||||
result = real_serializer.call
|
||||
|
||||
expect(result[:id]).to eq(real_visit.id)
|
||||
expect(result[:area_id]).to eq(real_visit.area_id)
|
||||
expect(result[:user_id]).to eq(real_visit.user_id)
|
||||
expect(result[:started_at]).to eq(real_visit.started_at)
|
||||
expect(result[:ended_at]).to eq(real_visit.ended_at)
|
||||
expect(result[:duration]).to eq(real_visit.duration)
|
||||
expect(result[:name]).to eq(real_visit.name)
|
||||
expect(result[:status]).to eq(real_visit.status)
|
||||
|
||||
expect(result[:place][:id]).to eq(real_place.id)
|
||||
expect(result[:place][:latitude]).to eq(real_place.lat)
|
||||
expect(result[:place][:longitude]).to eq(real_place.lon)
|
||||
end
|
||||
expect(result[:place][:id]).to eq(place.id)
|
||||
expect(result[:place][:latitude]).to eq(place.lat)
|
||||
expect(result[:place][:longitude]).to eq(place.lon)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
Loading…
Reference in a new issue