Add places page

This commit is contained in:
Eugene Burmakin 2024-08-25 20:19:02 +02:00
parent 0e90bdf15d
commit 7d7005063b
20 changed files with 202 additions and 52 deletions

View file

@ -22,7 +22,7 @@ The visit suggestion release.
- [x] Make it look acceptable
- [ ] Make visits suggestion an idempotent process
- [ ] Places management: if visit is detected at a place with a name, suggest this name as a visit name
- [ ] Separate page for places management
- [x] Separate page for places management
### Added

File diff suppressed because one or more lines are too long

View file

@ -1,8 +1,6 @@
# frozen_string_literal: true
class Api::V1::AreasController < ApplicationController
skip_forgery_protection
before_action :authenticate_api_key
class Api::V1::AreasController < ApiController
before_action :set_area, only: %i[update destroy]
def index

View file

@ -1,9 +1,6 @@
# frozen_string_literal: true
class Api::V1::Overland::BatchesController < ApplicationController
skip_forgery_protection
before_action :authenticate_api_key
class Api::V1::Overland::BatchesController < ApiController
def create
Overland::BatchCreatingJob.perform_later(batch_params, current_api_user.id)

View file

@ -1,9 +1,6 @@
# frozen_string_literal: true
class Api::V1::Owntracks::PointsController < ApplicationController
skip_forgery_protection
before_action :authenticate_api_key
class Api::V1::Owntracks::PointsController < ApiController
def create
Owntracks::PointCreatingJob.perform_later(point_params, current_api_user.id)

View file

@ -1,9 +1,6 @@
# frozen_string_literal: true
class Api::V1::PointsController < ApplicationController
skip_forgery_protection
before_action :authenticate_api_key
class Api::V1::PointsController < ApiController
def index
start_at = params[:start_at]&.to_datetime&.to_i
end_at = params[:end_at]&.to_datetime&.to_i || Time.zone.now.to_i

View file

@ -1,9 +1,6 @@
# frozen_string_literal: true
class Api::V1::StatsController < ApplicationController
skip_forgery_protection
before_action :authenticate_api_key
class Api::V1::StatsController < ApiController
def index
render json: StatsSerializer.new(current_api_user).call
end

View file

@ -1,9 +1,6 @@
# frozen_string_literal: true
class Api::V1::VisitsController < ApplicationController
skip_forgery_protection
before_action :authenticate_api_key
class Api::V1::VisitsController < ApiController
def update
visit = current_api_user.visits.find(params[:id])
visit = update_visit(visit)
@ -23,7 +20,7 @@ class Api::V1::VisitsController < ApplicationController
visit.name = visit.place.name if visit_params[:place_id].present?
end
visit.save
visit.save!
visit
end

View file

@ -0,0 +1,18 @@
# frozen_string_literal: true
class ApiController < ApplicationController
skip_before_action :verify_authenticity_token
before_action :authenticate_api_key
private
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

View file

@ -18,14 +18,4 @@ class ApplicationController < ActionController::Base
redirect_to root_path, notice: 'You are not authorized to perform this action.', status: :see_other
end
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

View file

@ -0,0 +1,22 @@
# frozen_string_literal: true
class PlacesController < ApplicationController
before_action :authenticate_user!
before_action :set_place, only: :destroy
def index
@places = current_user.places.page(params[:page]).per(20)
end
def destroy
@place.destroy!
redirect_to places_url, notice: 'Place was successfully destroyed.', status: :see_other
end
private
def set_place
@place = current_user.places.find(params[:id])
end
end

View file

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

View file

@ -6,14 +6,15 @@ class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :imports, dependent: :destroy
has_many :points, through: :imports
has_many :stats, dependent: :destroy
has_many :tracked_points, class_name: 'Point', dependent: :destroy
has_many :exports, dependent: :destroy
has_many :notifications, dependent: :destroy
has_many :areas, dependent: :destroy
has_many :visits, dependent: :destroy
has_many :imports, dependent: :destroy
has_many :stats, dependent: :destroy
has_many :exports, dependent: :destroy
has_many :notifications, dependent: :destroy
has_many :areas, dependent: :destroy
has_many :visits, dependent: :destroy
has_many :points, through: :imports
has_many :places, through: :visits
after_create :create_api_key

View file

@ -0,0 +1,52 @@
<% content_for :title, "Places" %>
<div class="w-full">
<div class="flex justify-center my-5">
<h1 class="font-bold text-4xl">Places</h1>
</div>
<div id="places" class="min-w-full">
<% if @places.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 places, created by Visits suggestion process. But now there are none.
</p>
</div>
</div>
</div>
<% else %>
<div class="flex justify-center my-5">
<div class='flex'>
<%= paginate @places %>
</div>
</div>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Created at</th>
<th>Coordinates</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<% @places.each do |place| %>
<tr>
<td><%= place.name %></td>
<td><%= place.created_at.strftime('%Y-%m-%d %H:%M:%S') %></td>
<td><%= place.to_coordinates.map(&:to_f) %></td>
<td>
<%= link_to 'Delete', place, data: { confirm: "Are you sure?", turbo_confirm: "Are you sure?", turbo_method: :delete }, method: :delete, class: "px-4 py-2 bg-red-500 text-white rounded-md" %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>
</div>

View file

@ -8,7 +8,8 @@
<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<sup>β</sup>'.html_safe, visits_url, class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Visits<sup>β</sup>'.html_safe, visits_url(status: :confirmed), class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Places', places_url, class: "#{active_class?(places_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 +43,8 @@
<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<sup>β</sup>'.html_safe, visits_url, class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Visits<sup>β</sup>'.html_safe, visits_url(status: :confirmed), class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Places', places_url, class: "#{active_class?(places_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>

View file

@ -27,6 +27,7 @@ Rails.application.routes.draw do
resources :imports
resources :visits, only: %i[index update]
resources :places, only: %i[index destroy]
resources :exports, only: %i[index create destroy]
resources :points, only: %i[index] do
collection do

View file

@ -12,6 +12,7 @@ RSpec.describe User, type: :model do
it { is_expected.to have_many(:notifications).dependent(:destroy) }
it { is_expected.to have_many(:areas).dependent(:destroy) }
it { is_expected.to have_many(:visits).dependent(:destroy) }
it { is_expected.to have_many(:places).through(:visits) }
end
describe 'callbacks' do

View file

@ -1,7 +1,50 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe "Api::V1::Visits", type: :request do
describe "GET /index" do
pending "add some examples (or delete) #{__FILE__}"
RSpec.describe 'Api::V1::Visits', type: :request do
let(:user) { create(:user) }
let(:api_key) { user.api_key }
describe 'PUT /api/v1/visits/:id' do
let(:visit) { create(:visit, user:) }
let(:valid_attributes) do
{
visit: {
name: 'New name'
}
}
end
let(:invalid_attributes) do
{
visit: {
name: nil
}
}
end
context 'with valid parameters' do
it 'updates the requested visit' do
put api_v1_visit_url(visit, api_key:), params: valid_attributes
expect(visit.reload.name).to eq('New name')
end
it 'renders a JSON response with the visit' do
put api_v1_visit_url(visit, api_key:), params: valid_attributes
expect(response).to have_http_status(:ok)
end
end
context 'with invalid parameters' do
it 'renders a JSON response with errors for the visit' do
put api_v1_visit_url(visit, api_key:), params: invalid_attributes
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
end

View file

@ -0,0 +1,39 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe '/places', type: :request do
let(:user) { create(:user) }
before do
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
sign_in user
end
describe 'GET /index' do
it 'renders a successful response' do
get places_url
expect(response).to be_successful
end
end
describe 'DELETE /destroy' do
let!(:place) { create(:place) }
let!(:visit) { create(:visit, place:, user:) }
it 'destroys the requested place' do
expect do
delete place_url(place)
end.to change(Place, :count).by(-1)
end
it 'redirects to the places list' do
delete place_url(place)
expect(response).to redirect_to(places_url)
end
end
end

View file

@ -42,8 +42,8 @@ RSpec.describe Visits::Suggest do
expect { subject }.to change(Notification, :count).by(1)
end
it 'does not reverse geocodes visits' do
expect_any_instance_of(Visit).to_not receive(:async_reverse_geocode).and_call_original
it 'reverse geocodes visits' do
expect_any_instance_of(Visit).to receive(:async_reverse_geocode).and_call_original
subject
end