mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Fix route popup distance and add areas swagger test
This commit is contained in:
parent
ffe0334ebc
commit
87258df41e
18 changed files with 332 additions and 172 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
|
@ -6,7 +6,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
|||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
|
||||
## [0.9.5] — 2024-07-22
|
||||
## [0.9.5] — 2024-07-26
|
||||
|
||||
### Added
|
||||
|
||||
|
|
@ -14,9 +14,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
|||
- A background job to calculate your visits. This job will calculate your visits based on the areas you've created.
|
||||
- Visits page. This page will show you all your visits, calculated based on the areas you've created. You can see the date and time of the visit, the area you've visited, and the duration of the visit.
|
||||
- A possibility to confirm or decline a visit. When you create an area, the visit is not calculated immediately. You need to confirm or decline the visit. You can do this on the Visits page. Click on the visit, then click on the "Confirm" or "Decline" button. If you confirm the visit, it will be added to your timeline. If you decline the visit, it will be removed from your timeline.
|
||||
- [ ] Glue two consecutive visits if there are no points between them
|
||||
- [ ] Group visits by day and paginate them
|
||||
- [ ] Tests
|
||||
- [x] Swagger doc for Areas endpoint
|
||||
- [ ] Atomicity for visits creation
|
||||
|
||||
### Fixed
|
||||
|
||||
- A route popup now correctly shows distance made in the route, not the distance between first and last points in the route.
|
||||
---
|
||||
|
||||
## [0.9.4] — 2024-07-21
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -1,17 +1,17 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::AreasController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
before_action :authenticate_api_key
|
||||
before_action :set_area, only: %i[update destroy]
|
||||
|
||||
def index
|
||||
@areas = current_user.areas
|
||||
@areas = current_api_user.areas
|
||||
|
||||
render json: @areas, status: :ok
|
||||
end
|
||||
|
||||
def create
|
||||
@area = current_user.areas.build(area_params)
|
||||
@area = current_api_user.areas.build(area_params)
|
||||
|
||||
if @area.save
|
||||
render json: @area, status: :created
|
||||
|
|
@ -37,7 +37,7 @@ class Api::V1::AreasController < ApplicationController
|
|||
private
|
||||
|
||||
def set_area
|
||||
@area = current_user.areas.find(params[:id])
|
||||
@area = current_api_user.areas.find(params[:id])
|
||||
end
|
||||
|
||||
def area_params
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ class Api::V1::PointsController < ApplicationController
|
|||
before_action :authenticate_user!
|
||||
|
||||
def destroy
|
||||
point = current_user.points.find(params[:id])
|
||||
point = current_user.tracked_points.find(params[:id])
|
||||
point.destroy
|
||||
|
||||
render json: { message: 'Point deleted successfully' }
|
||||
|
|
|
|||
|
|
@ -10,17 +10,9 @@ module ApplicationHelper
|
|||
end
|
||||
end
|
||||
|
||||
def month_timespan(stat)
|
||||
month = DateTime.new(stat.year, stat.month).in_time_zone(Time.zone)
|
||||
start_at = month.beginning_of_month.to_time.strftime('%Y-%m-%dT%H:%M')
|
||||
end_at = month.end_of_month.to_time.strftime('%Y-%m-%dT%H:%M')
|
||||
|
||||
{ start_at:, end_at: }
|
||||
end
|
||||
|
||||
def year_timespan(year)
|
||||
start_at = Time.utc(year).in_time_zone('Europe/Berlin').beginning_of_year.strftime('%Y-%m-%dT%H:%M')
|
||||
end_at = Time.utc(year).in_time_zone('Europe/Berlin').end_of_year.strftime('%Y-%m-%dT%H:%M')
|
||||
start_at = Time.utc(year).in_time_zone(Time.zone).beginning_of_year.strftime('%Y-%m-%dT%H:%M')
|
||||
end_at = Time.utc(year).in_time_zone(Time.zone).end_of_year.strftime('%Y-%m-%dT%H:%M')
|
||||
|
||||
{ start_at:, end_at: }
|
||||
end
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export default class extends Controller {
|
|||
connect() {
|
||||
console.log("Map controller connected");
|
||||
|
||||
this.apiKey = this.element.dataset.api_key;
|
||||
this.markers = JSON.parse(this.element.dataset.coordinates);
|
||||
this.timezone = this.element.dataset.timezone;
|
||||
this.clearFogRadius = this.element.dataset.fog_of_war_meters;
|
||||
|
|
@ -233,24 +234,26 @@ export default class extends Controller {
|
|||
fog.appendChild(circle);
|
||||
}
|
||||
|
||||
addHighlightOnHover(polyline, map, startPoint, endPoint, prevPoint, nextPoint, timezone) {
|
||||
addHighlightOnHover(polyline, map, polylineCoordinates, timezone) {
|
||||
const originalStyle = { color: "blue", opacity: 0.6, weight: 3 };
|
||||
const highlightStyle = { color: "yellow", opacity: 1, weight: 5 };
|
||||
|
||||
polyline.setStyle(originalStyle);
|
||||
|
||||
const startPoint = polylineCoordinates[0];
|
||||
const endPoint = polylineCoordinates[polylineCoordinates.length - 1];
|
||||
|
||||
const firstTimestamp = new Date(startPoint[4] * 1000).toLocaleString("en-GB", { timeZone: timezone });
|
||||
const lastTimestamp = new Date(endPoint[4] * 1000).toLocaleString("en-GB", { timeZone: timezone });
|
||||
|
||||
const minutes = Math.round((endPoint[4] - startPoint[4]) / 60);
|
||||
const timeOnRoute = minutesToDaysHoursMinutes(minutes);
|
||||
const distance = haversineDistance(startPoint[0], startPoint[1], endPoint[0], endPoint[1]);
|
||||
|
||||
const distanceToPrev = prevPoint ? haversineDistance(prevPoint[0], prevPoint[1], startPoint[0], startPoint[1]) : "N/A";
|
||||
const distanceToNext = nextPoint ? haversineDistance(endPoint[0], endPoint[1], nextPoint[0], nextPoint[1]) : "N/A";
|
||||
|
||||
const timeBetweenPrev = prevPoint ? Math.round((startPoint[4] - prevPoint[4]) / 60) : "N/A";
|
||||
const timeBetweenNext = nextPoint ? Math.round((nextPoint[4] - endPoint[4]) / 60) : "N/A";
|
||||
const totalDistance = polylineCoordinates.reduce((acc, curr, index, arr) => {
|
||||
if (index === 0) return acc;
|
||||
const dist = haversineDistance(arr[index - 1][0], arr[index - 1][1], curr[0], curr[1]);
|
||||
return acc + dist;
|
||||
}, 0);
|
||||
|
||||
const startIcon = L.divIcon({ html: "🚥", className: "emoji-icon" });
|
||||
const finishIcon = L.divIcon({ html: "🏁", className: "emoji-icon" });
|
||||
|
|
@ -261,10 +264,18 @@ export default class extends Controller {
|
|||
<b>Start:</b> ${firstTimestamp}<br>
|
||||
<b>End:</b> ${lastTimestamp}<br>
|
||||
<b>Duration:</b> ${timeOnRoute}<br>
|
||||
<b>Distance:</b> ${formatDistance(distance)}<br>
|
||||
<b>Total Distance:</b> ${formatDistance(totalDistance)}<br>
|
||||
`;
|
||||
|
||||
if (isDebugMode) {
|
||||
const prevPoint = polylineCoordinates[0];
|
||||
const nextPoint = polylineCoordinates[polylineCoordinates.length - 1];
|
||||
const distanceToPrev = haversineDistance(prevPoint[0], prevPoint[1], startPoint[0], startPoint[1]);
|
||||
const distanceToNext = haversineDistance(endPoint[0], endPoint[1], nextPoint[0], nextPoint[1]);
|
||||
|
||||
const timeBetweenPrev = Math.round((startPoint[4] - prevPoint[4]) / 60);
|
||||
const timeBetweenNext = Math.round((endPoint[4] - nextPoint[4]) / 60);
|
||||
|
||||
popupContent += `
|
||||
<b>Prev Route:</b> ${Math.round(distanceToPrev)}m and ${minutesToDaysHoursMinutes(timeBetweenPrev)} away<br>
|
||||
<b>Next Route:</b> ${Math.round(distanceToNext)}m and ${minutesToDaysHoursMinutes(timeBetweenNext)} away<br>
|
||||
|
|
@ -341,12 +352,7 @@ export default class extends Controller {
|
|||
const latLngs = polylineCoordinates.map((point) => [point[0], point[1]]);
|
||||
const polyline = L.polyline(latLngs, { color: "blue", opacity: 0.6, weight: 3 });
|
||||
|
||||
const startPoint = polylineCoordinates[0];
|
||||
const endPoint = polylineCoordinates[polylineCoordinates.length - 1];
|
||||
const prevPoint = index > 0 ? splitPolylines[index - 1][splitPolylines[index - 1].length - 1] : null;
|
||||
const nextPoint = index < splitPolylines.length - 1 ? splitPolylines[index + 1][0] : null;
|
||||
|
||||
this.addHighlightOnHover(polyline, map, startPoint, endPoint, prevPoint, nextPoint, timezone);
|
||||
this.addHighlightOnHover(polyline, map, polylineCoordinates, timezone);
|
||||
|
||||
return polyline;
|
||||
})
|
||||
|
|
@ -433,7 +439,7 @@ export default class extends Controller {
|
|||
this.areasLayer.addLayer(layer);
|
||||
}
|
||||
|
||||
saveCircle(formData, layer) {
|
||||
saveCircle(formData, layer, apiKey) {
|
||||
const data = {};
|
||||
formData.forEach((value, key) => {
|
||||
const keys = key.split('[').map(k => k.replace(']', ''));
|
||||
|
|
@ -445,12 +451,9 @@ export default class extends Controller {
|
|||
}
|
||||
});
|
||||
|
||||
fetch('/api/v1/areas', {
|
||||
fetch(`"/api/v1/areas?api_key=${apiKey}"`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').getAttribute('content')
|
||||
},
|
||||
headers: { 'Content-Type': 'application/json'},
|
||||
body: JSON.stringify(data)
|
||||
})
|
||||
.then(response => {
|
||||
|
|
|
|||
83
app/services/areas/visits/create.rb
Normal file
83
app/services/areas/visits/create.rb
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Areas::Visits::Create
|
||||
attr_reader :user, :areas
|
||||
|
||||
def initialize(user, areas)
|
||||
@user = user
|
||||
@areas = areas
|
||||
@time_threshold_minutes = 30 || user.settings['time_threshold_minutes']
|
||||
@merge_threshold_minutes = 15 || user.settings['merge_threshold_minutes']
|
||||
end
|
||||
|
||||
def call
|
||||
areas.map { area_visits(_1) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def area_visits(area)
|
||||
points_grouped_by_month = area_points(area)
|
||||
visits_by_month = group_points_by_month(points_grouped_by_month)
|
||||
|
||||
visits_by_month.each do |month, visits|
|
||||
Rails.logger.info("Month: #{month}, Total visits: #{visits.size}")
|
||||
|
||||
visits.each do |time_range, visit_points|
|
||||
create_or_update_visit(area, time_range, visit_points)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def area_points(area)
|
||||
area_radius_in_km = area.radius / 1000.0
|
||||
|
||||
points = Point.where(user_id: user.id)
|
||||
.near([area.latitude, area.longitude], area_radius_in_km)
|
||||
.order(timestamp: :asc)
|
||||
|
||||
# check if all points within the area are assigned to a visit
|
||||
|
||||
points.group_by { |point| Time.zone.at(point.timestamp).strftime('%Y-%m') }
|
||||
end
|
||||
|
||||
def group_points_by_month(points)
|
||||
visits_by_month = {}
|
||||
|
||||
points.each do |month, points_in_month|
|
||||
visits_by_month[month] = Visits::Group.new(
|
||||
time_threshold_minutes: @time_threshold_minutes,
|
||||
merge_threshold_minutes: @merge_threshold_minutes
|
||||
).call(points_in_month)
|
||||
end
|
||||
|
||||
visits_by_month
|
||||
end
|
||||
|
||||
def create_or_update_visit(area, time_range, visit_points)
|
||||
Rails.logger.info("Visit from #{time_range}, Points: #{visit_points.size}")
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
visit = find_visit(area.id, visit_points.first.timestamp)
|
||||
|
||||
visit.tap do |v|
|
||||
v.name = "#{area.name}, #{time_range}"
|
||||
v.ended_at = Time.zone.at(visit_points.last.timestamp)
|
||||
v.duration = (visit_points.last.timestamp - visit_points.first.timestamp) / 60
|
||||
v.status = :pending
|
||||
end
|
||||
|
||||
visit.save!
|
||||
|
||||
visit_points.each { _1.update!(visit_id: visit.id) }
|
||||
end
|
||||
end
|
||||
|
||||
def find_visit(area_id, timestamp)
|
||||
Visit.find_or_initialize_by(
|
||||
area_id:,
|
||||
user_id: user.id,
|
||||
started_at: Time.zone.at(timestamp)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Visits::Areas::Calculate
|
||||
attr_reader :user, :areas
|
||||
|
||||
def initialize(user, areas)
|
||||
@user = user
|
||||
@areas = areas
|
||||
@time_threshold_minutes = 30 || user.settings['time_threshold_minutes']
|
||||
@merge_threshold_minutes = 15 || user.settings['merge_threshold_minutes']
|
||||
end
|
||||
|
||||
def call
|
||||
areas.map { area_visits(_1) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def area_visits(area)
|
||||
points_grouped_by_month = area_points(area)
|
||||
visits_by_month = {}
|
||||
|
||||
points_grouped_by_month.each do |month, points_in_month|
|
||||
visits_by_month[month] = Visits::Group.new(
|
||||
time_threshold_minutes: @time_threshold_minutes,
|
||||
merge_threshold_minutes: @merge_threshold_minutes
|
||||
).call(points_in_month)
|
||||
end
|
||||
|
||||
visits_by_month.each do |month, visits|
|
||||
Rails.logger.info("Month: #{month}, Total visits: #{visits.size}")
|
||||
|
||||
visits.each do |time_range, visit_points|
|
||||
Rails.logger.info("Visit from #{time_range}, Points: #{visit_points.size}")
|
||||
|
||||
ActiveRecord::Base.transaction do
|
||||
visit = Visit.find_or_initialize_by(
|
||||
area_id: area.id,
|
||||
user_id: user.id,
|
||||
started_at: Time.zone.at(visit_points.first.timestamp)
|
||||
)
|
||||
|
||||
visit.update!(
|
||||
name: "#{area.name}, #{time_range}",
|
||||
area_id: area.id,
|
||||
user_id: user.id,
|
||||
started_at: Time.zone.at(visit_points.first.timestamp),
|
||||
ended_at: Time.zone.at(visit_points.last.timestamp),
|
||||
duration: (visit_points.last.timestamp - visit_points.first.timestamp) / 60, # in minutes
|
||||
status: :pending
|
||||
)
|
||||
|
||||
visit_points.each { _1.update!(visit_id: visit.id) }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def area_points(area)
|
||||
area_radius_in_km = area.radius / 1000.0
|
||||
|
||||
Point.where(user_id: user.id)
|
||||
.near([area.latitude, area.longitude], area_radius_in_km)
|
||||
.order(timestamp: :asc)
|
||||
.group_by { |point| Time.zone.at(point.timestamp).strftime('%Y-%m') }
|
||||
end
|
||||
end
|
||||
|
|
@ -73,6 +73,7 @@ class Visits::Group
|
|||
previous_visit = visit
|
||||
else
|
||||
time_difference = (visit.start_time - previous_visit.end_time) / 60.0
|
||||
|
||||
if time_difference <= @merge_threshold_minutes
|
||||
merge_visit(previous_visit, visit)
|
||||
else
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@
|
|||
|
||||
<div
|
||||
class="w-full"
|
||||
data-api_key="<%= current_user.api_key %>"
|
||||
data-controller="maps"
|
||||
data-coordinates="<%= @coordinates %>"
|
||||
data-timezone="<%= Rails.configuration.time_zone %>"
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<li><%= link_to 'Map', map_url, class: "#{active_class?(map_url)}" %></li>
|
||||
<li><%= link_to 'Points', points_url, class: "#{active_class?(points_url)}" %></li>
|
||||
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
|
||||
<li><%= link_to 'Visits', visits_url, class: "#{active_class?(visits_url)}" %></li>
|
||||
<li><%= link_to 'Visits<sup>β</sup>'.html_safe, visits_url, class: "#{active_class?(visits_url)}" %></li>
|
||||
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
|
||||
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %></li>
|
||||
</ul>
|
||||
|
|
@ -42,7 +42,7 @@
|
|||
<li><%= link_to 'Map', map_url, class: "#{active_class?(map_url)}" %></li>
|
||||
<li><%= link_to 'Points', points_url, class: "#{active_class?(points_url)}" %></li>
|
||||
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
|
||||
<li><%= link_to 'Visits', visits_url, class: "#{active_class?(visits_url)}" %></li>
|
||||
<li><%= link_to 'Visits<sup>β</sup>'.html_safe, visits_url, class: "#{active_class?(visits_url)}" %></li>
|
||||
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
|
||||
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %></li>
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<div id="<%= dom_id stat %>" class="card w-full bg-base-200 shadow-xl">
|
||||
<div class="card-body">
|
||||
<h2 class="card-title">
|
||||
<%= link_to map_url(month_timespan(stat)), class: "underline hover:no-underline text-#{header_colors.sample}" do %>
|
||||
<%= link_to map_url(timespan(stat.month, stat.year)), class: "underline hover:no-underline text-#{header_colors.sample}" do %>
|
||||
<%= "#{Date::MONTHNAMES[stat.month]} of #{stat.year}" %>
|
||||
<% end %>
|
||||
</h2>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
<div class="w-full">
|
||||
<% content_for :title, "Visits" %>
|
||||
|
||||
<div class="flex justify-between items-center">
|
||||
<div class="flex justify-center my-5">
|
||||
<h1 class="font-bold text-4xl">Visits</h1>
|
||||
</div>
|
||||
|
||||
<%= paginate @visits %>
|
||||
<div class="flex justify-center my-5">
|
||||
<div class='flex'>
|
||||
<%= paginate @visits %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ul class="timeline timeline-snap-icon max-md:timeline-compact timeline-vertical">
|
||||
<% @visits.each.with_index do |date, index| %>
|
||||
|
|
|
|||
|
|
@ -4,5 +4,10 @@ FactoryBot.define do
|
|||
factory :visit do
|
||||
area
|
||||
user
|
||||
started_at { Time.zone.now }
|
||||
ended_at { Time.zone.now + 1.hour }
|
||||
duration { 1.hour }
|
||||
name { 'Visit' }
|
||||
status { 'pending' }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,11 +5,9 @@ require 'rails_helper'
|
|||
RSpec.describe '/api/v1/areas', type: :request do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before { sign_in user }
|
||||
|
||||
describe 'GET /index' do
|
||||
it 'renders a successful response' do
|
||||
get api_v1_areas_url
|
||||
get api_v1_areas_url(api_key: user.api_key)
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
|
@ -22,12 +20,12 @@ RSpec.describe '/api/v1/areas', type: :request do
|
|||
|
||||
it 'creates a new Area' do
|
||||
expect do
|
||||
post api_v1_areas_url, params: { area: valid_attributes }
|
||||
post api_v1_areas_url(api_key: user.api_key), params: { area: valid_attributes }
|
||||
end.to change(Area, :count).by(1)
|
||||
end
|
||||
|
||||
it 'redirects to the created api_v1_area' do
|
||||
post api_v1_areas_url, params: { area: valid_attributes }
|
||||
post api_v1_areas_url(api_key: user.api_key), params: { area: valid_attributes }
|
||||
|
||||
expect(response).to have_http_status(:created)
|
||||
end
|
||||
|
|
@ -40,12 +38,12 @@ RSpec.describe '/api/v1/areas', type: :request do
|
|||
|
||||
it 'does not create a new Area' do
|
||||
expect do
|
||||
post api_v1_areas_url, params: { area: invalid_attributes }
|
||||
post api_v1_areas_url(api_key: user.api_key), params: { area: invalid_attributes }
|
||||
end.to change(Area, :count).by(0)
|
||||
end
|
||||
|
||||
it 'renders a response with 422 status' do
|
||||
post api_v1_areas_url, params: { area: invalid_attributes }
|
||||
post api_v1_areas_url(api_key: user.api_key), params: { area: invalid_attributes }
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
|
|
@ -58,14 +56,14 @@ RSpec.describe '/api/v1/areas', type: :request do
|
|||
let(:new_attributes) { attributes_for(:area).merge(name: 'New Name') }
|
||||
|
||||
it 'updates the requested api_v1_area' do
|
||||
patch api_v1_area_url(area), params: { area: new_attributes }
|
||||
patch api_v1_area_url(area, api_key: user.api_key), params: { area: new_attributes }
|
||||
area.reload
|
||||
|
||||
expect(area.reload.name).to eq('New Name')
|
||||
end
|
||||
|
||||
it 'redirects to the api_v1_area' do
|
||||
patch api_v1_area_url(area), params: { area: new_attributes }
|
||||
patch api_v1_area_url(area, api_key: user.api_key), params: { area: new_attributes }
|
||||
area.reload
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
|
|
@ -77,7 +75,7 @@ RSpec.describe '/api/v1/areas', type: :request do
|
|||
let(:invalid_attributes) { attributes_for(:area, name: nil) }
|
||||
|
||||
it 'renders a response with 422 status' do
|
||||
patch api_v1_area_url(area), params: { area: invalid_attributes }
|
||||
patch api_v1_area_url(area, api_key: user.api_key), params: { area: invalid_attributes }
|
||||
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
|
@ -89,12 +87,12 @@ RSpec.describe '/api/v1/areas', type: :request do
|
|||
|
||||
it 'destroys the requested api_v1_area' do
|
||||
expect do
|
||||
delete api_v1_area_url(area)
|
||||
delete api_v1_area_url(area, api_key: user.api_key)
|
||||
end.to change(Area, :count).by(-1)
|
||||
end
|
||||
|
||||
it 'redirects to the api_v1_areas list' do
|
||||
delete api_v1_area_url(area)
|
||||
delete api_v1_area_url(area, api_key: user.api_key)
|
||||
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,94 +12,84 @@ require 'rails_helper'
|
|||
# of tools you can use to make these specs even more expressive, but we're
|
||||
# sticking to rails and rspec-rails APIs to keep things simple and stable.
|
||||
|
||||
RSpec.describe "/visits", type: :request do
|
||||
|
||||
RSpec.describe '/visits', type: :request do
|
||||
# This should return the minimal set of attributes required to create a valid
|
||||
# Visit. As you add validations to Visit, be sure to
|
||||
# adjust the attributes here as well.
|
||||
let(:valid_attributes) {
|
||||
skip("Add a hash of attributes valid for your model")
|
||||
}
|
||||
let(:valid_attributes) do
|
||||
skip('Add a hash of attributes valid for your model')
|
||||
end
|
||||
|
||||
let(:invalid_attributes) {
|
||||
skip("Add a hash of attributes invalid for your model")
|
||||
}
|
||||
let(:invalid_attributes) do
|
||||
skip('Add a hash of attributes invalid for your model')
|
||||
end
|
||||
|
||||
describe "GET /index" do
|
||||
it "renders a successful response" do
|
||||
describe 'GET /index' do
|
||||
it 'renders a successful response' do
|
||||
Visit.create! valid_attributes
|
||||
get visits_url
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /show" do
|
||||
it "renders a successful response" do
|
||||
describe 'GET /show' do
|
||||
it 'renders a successful response' do
|
||||
visit = Visit.create! valid_attributes
|
||||
get visit_url(visit)
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /new" do
|
||||
it "renders a successful response" do
|
||||
get new_visit_url
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /edit" do
|
||||
it "renders a successful response" do
|
||||
describe 'GET /edit' do
|
||||
it 'renders a successful response' do
|
||||
visit = Visit.create! valid_attributes
|
||||
get edit_visit_url(visit)
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /create" do
|
||||
context "with valid parameters" do
|
||||
it "creates a new Visit" do
|
||||
expect {
|
||||
describe 'POST /create' do
|
||||
context 'with valid parameters' do
|
||||
it 'creates a new Visit' do
|
||||
expect do
|
||||
post visits_url, params: { visit: valid_attributes }
|
||||
}.to change(Visit, :count).by(1)
|
||||
end.to change(Visit, :count).by(1)
|
||||
end
|
||||
|
||||
it "redirects to the created visit" do
|
||||
it 'redirects to the created visit' do
|
||||
post visits_url, params: { visit: valid_attributes }
|
||||
expect(response).to redirect_to(visit_url(Visit.last))
|
||||
end
|
||||
end
|
||||
|
||||
context "with invalid parameters" do
|
||||
it "does not create a new Visit" do
|
||||
expect {
|
||||
context 'with invalid parameters' do
|
||||
it 'does not create a new Visit' do
|
||||
expect do
|
||||
post visits_url, params: { visit: invalid_attributes }
|
||||
}.to change(Visit, :count).by(0)
|
||||
end.to change(Visit, :count).by(0)
|
||||
end
|
||||
|
||||
|
||||
it "renders a response with 422 status (i.e. to display the 'new' template)" do
|
||||
post visits_url, params: { visit: invalid_attributes }
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
describe "PATCH /update" do
|
||||
context "with valid parameters" do
|
||||
let(:new_attributes) {
|
||||
skip("Add a hash of attributes valid for your model")
|
||||
}
|
||||
describe 'PATCH /update' do
|
||||
context 'with valid parameters' do
|
||||
let(:new_attributes) do
|
||||
skip('Add a hash of attributes valid for your model')
|
||||
end
|
||||
|
||||
it "updates the requested visit" do
|
||||
it 'updates the requested visit' do
|
||||
visit = Visit.create! valid_attributes
|
||||
patch visit_url(visit), params: { visit: new_attributes }
|
||||
visit.reload
|
||||
skip("Add assertions for updated state")
|
||||
skip('Add assertions for updated state')
|
||||
end
|
||||
|
||||
it "redirects to the visit" do
|
||||
it 'redirects to the visit' do
|
||||
visit = Visit.create! valid_attributes
|
||||
patch visit_url(visit), params: { visit: new_attributes }
|
||||
visit.reload
|
||||
|
|
@ -107,26 +97,24 @@ RSpec.describe "/visits", type: :request do
|
|||
end
|
||||
end
|
||||
|
||||
context "with invalid parameters" do
|
||||
|
||||
context 'with invalid parameters' do
|
||||
it "renders a response with 422 status (i.e. to display the 'edit' template)" do
|
||||
visit = Visit.create! valid_attributes
|
||||
patch visit_url(visit), params: { visit: invalid_attributes }
|
||||
expect(response).to have_http_status(:unprocessable_entity)
|
||||
end
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /destroy" do
|
||||
it "destroys the requested visit" do
|
||||
describe 'DELETE /destroy' do
|
||||
it 'destroys the requested visit' do
|
||||
visit = Visit.create! valid_attributes
|
||||
expect {
|
||||
expect do
|
||||
delete visit_url(visit)
|
||||
}.to change(Visit, :count).by(-1)
|
||||
end.to change(Visit, :count).by(-1)
|
||||
end
|
||||
|
||||
it "redirects to the visits list" do
|
||||
it 'redirects to the visits list' do
|
||||
visit = Visit.create! valid_attributes
|
||||
delete visit_url(visit)
|
||||
expect(response).to redirect_to(visits_url)
|
||||
|
|
|
|||
67
spec/swagger/api/v1/areas_controller_spec.rb
Normal file
67
spec/swagger/api/v1/areas_controller_spec.rb
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'swagger_helper'
|
||||
|
||||
describe 'Areas API', type: :request do
|
||||
path '/api/v1/areas' do
|
||||
post 'Creates an area' do
|
||||
request_body_example value: {
|
||||
'name': 'Home',
|
||||
'latitude': 40.7128,
|
||||
'longitude': -74.0060,
|
||||
'radius': 100
|
||||
}
|
||||
tags 'Areas'
|
||||
consumes 'application/json'
|
||||
parameter name: :area, in: :body, schema: {
|
||||
type: :object,
|
||||
properties: {
|
||||
name: { type: :string },
|
||||
latitude: { type: :number },
|
||||
longitude: { type: :number },
|
||||
radius: { type: :number }
|
||||
},
|
||||
required: %w[name latitude longitude radius]
|
||||
}
|
||||
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
|
||||
response '201', 'area created' do
|
||||
let(:area) { { name: 'Home', latitude: 40.7128, longitude: -74.0060, radius: 100 } }
|
||||
let(:api_key) { create(:user).api_key }
|
||||
|
||||
run_test!
|
||||
end
|
||||
response '422', 'invalid request' do
|
||||
let(:area) { { name: 'Home', latitude: 40.7128, longitude: -74.0060 } }
|
||||
let(:api_key) { create(:user).api_key }
|
||||
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
|
||||
get 'Retrieves all areas' do
|
||||
tags 'Areas'
|
||||
produces 'application/json'
|
||||
parameter name: :api_key, in: :query, type: :string, required: true, description: 'API Key'
|
||||
response '200', 'areas found' do
|
||||
schema type: :array,
|
||||
items: {
|
||||
type: :object,
|
||||
properties: {
|
||||
id: { type: :integer },
|
||||
name: { type: :string },
|
||||
latitude: { type: :number },
|
||||
longitude: { type: :number },
|
||||
radius: { type: :number }
|
||||
},
|
||||
required: %w[id name latitude longitude radius]
|
||||
}
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:areas) { create_list(:area, 3, user:) }
|
||||
let(:api_key) { user.api_key }
|
||||
|
||||
run_test!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -4,6 +4,87 @@ info:
|
|||
title: API V1
|
||||
version: v1
|
||||
paths:
|
||||
"/api/v1/areas":
|
||||
post:
|
||||
summary: Creates an area
|
||||
tags:
|
||||
- Areas
|
||||
parameters:
|
||||
- name: api_key
|
||||
in: query
|
||||
required: true
|
||||
description: API Key
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'201':
|
||||
description: area created
|
||||
'422':
|
||||
description: invalid request
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
latitude:
|
||||
type: number
|
||||
longitude:
|
||||
type: number
|
||||
radius:
|
||||
type: number
|
||||
required:
|
||||
- name
|
||||
- latitude
|
||||
- longitude
|
||||
- radius
|
||||
examples:
|
||||
'0':
|
||||
summary: Creates an area
|
||||
value:
|
||||
name: Home
|
||||
latitude: 40.7128
|
||||
longitude: -74.006
|
||||
radius: 100
|
||||
get:
|
||||
summary: Retrieves all areas
|
||||
tags:
|
||||
- Areas
|
||||
parameters:
|
||||
- name: api_key
|
||||
in: query
|
||||
required: true
|
||||
description: API Key
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
'200':
|
||||
description: areas found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: integer
|
||||
name:
|
||||
type: string
|
||||
latitude:
|
||||
type: number
|
||||
longitude:
|
||||
type: number
|
||||
radius:
|
||||
type: number
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- latitude
|
||||
- longitude
|
||||
- radius
|
||||
"/api/v1/overland/batches":
|
||||
post:
|
||||
summary: Creates a batch of points
|
||||
|
|
|
|||
Loading…
Reference in a new issue