Add trips model and scaffold controller

This commit is contained in:
Eugene Burmakin 2024-11-27 20:14:17 +01:00
parent cb1665d8be
commit 198bf3128a
18 changed files with 434 additions and 1 deletions

View file

@ -0,0 +1,51 @@
# frozen_string_literal: true
class TripsController < ApplicationController
before_action :authenticate_user!
before_action :set_trip, only: %i[show edit update destroy]
def index
@trips = current_user.trips
end
def show; end
def new
@trip = Trip.new
end
def edit; end
def create
@trip = current_user.trips.build(trip_params)
if @trip.save
redirect_to @trip, notice: 'Trip was successfully created.'
else
render :new, status: :unprocessable_entity
end
end
def update
if @trip.update(trip_params)
redirect_to @trip, notice: 'Trip was successfully updated.', status: :see_other
else
render :edit, status: :unprocessable_entity
end
end
def destroy
@trip.destroy!
redirect_to trips_url, notice: 'Trip was successfully destroyed.', status: :see_other
end
private
def set_trip
@trip = current_user.trips.find(params[:id])
end
def trip_params
params.require(:trip).permit(:name, :started_at, :ended_at, :notes)
end
end

View file

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

7
app/models/trip.rb Normal file
View file

@ -0,0 +1,7 @@
# frozen_string_literal: true
class Trip < ApplicationRecord
belongs_to :user
validates :name, :started_at, :ended_at, presence: true
end

View file

@ -15,6 +15,7 @@ class User < ApplicationRecord
has_many :visits, dependent: :destroy
has_many :points, through: :imports
has_many :places, through: :visits
has_many :trips, dependent: :destroy
after_create :create_api_key

View file

@ -10,6 +10,7 @@
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
<li><%= link_to 'Visits<sup>β</sup>'.html_safe, visits_url(status: :confirmed), class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Places<sup>β</sup>'.html_safe, places_url, class: "#{active_class?(places_url)}" %></li>
<li><%= link_to 'Trips<sup>β</sup>'.html_safe, trips_url, class: "#{active_class?(trips_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>
@ -46,6 +47,7 @@
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
<li><%= link_to 'Visits<sup>β</sup>'.html_safe, visits_url(status: :confirmed), class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Places<sup>β</sup>'.html_safe, places_url, class: "#{active_class?(places_url)}" %></li>
<li><%= link_to 'Trips', trips_url, class: "#{active_class?(trips_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

@ -0,0 +1,39 @@
<%= form_with(model: trip, class: "contents") do |form| %>
<% if trip.errors.any? %>
<div id="error_explanation" class="bg-red-50 text-red-500 px-3 py-2 font-medium rounded-lg mt-3">
<h2><%= pluralize(trip.errors.count, "error") %> prohibited this trip from being saved:</h2>
<ul>
<% trip.errors.each do |error| %>
<li><%= error.full_message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="flex flex-col lg:flex-row gap-4 mt-4">
<div class="form-control w-full lg:w-1/2">
<%= form.label :name %>
<%= form.text_field :name, class: 'input input-bordered' %>
</div>
<div class="flex flex-col lg:flex-row lg:w-1/2 gap-4">
<div class="form-control w-full lg:w-1/2">
<%= form.label :started_at %>
<%= form.datetime_field :started_at, include_seconds: false, class: 'input input-bordered', value: trip.started_at %>
</div>
<div class="form-control w-full lg:w-1/2">
<%= form.label :ended_at %>
<%= form.datetime_field :ended_at, include_seconds: false, class: 'input input-bordered', value: trip.ended_at %>
</div>
</div>
</div>
<div class="form-control w-full mt-4 mb-4">
<%= form.label :notes %>
<%= form.text_area :notes, class: 'textarea textarea-bordered w-full', rows: 10 %>
</div>
<div class="inline mb-4">
<%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %>
</div>
<% end %>

View file

@ -0,0 +1,2 @@
<div id="<%= dom_id trip %>">
</div>

View file

@ -0,0 +1,8 @@
<div class="mx-auto md:w-2/3 w-full">
<h1 class="font-bold text-4xl">Editing trip</h1>
<%= render "form", trip: @trip %>
<%= link_to "Show this trip", @trip, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
<%= link_to "Back to trips", trips_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
</div>

View file

@ -0,0 +1,48 @@
<% content_for :title, 'Trips' %>
<div class="w-full">
<div id="trips" class="min-w-full">
<% if @trips.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 trips, but now there are none. Let's <%= link_to 'create one', new_trip_path, class: 'link' %>!
</p>
</div>
</div>
</div>
<% else %>
<div class="flex justify-center my-5">
<div class='flex'>
<%= paginate @trips %>
</div>
</div>
<div class="overflow-x-auto">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Started at</th>
<th>Ended at</th>
</tr>
</thead>
<tbody
data-controller="trips"
data-trips-target="index"
data-user-id="<%= current_user.id %>"
>
<% @trips.each do |trip| %>
<tr data-trip-id="<%= trip.id %>" id="trip-<%= trip.id %>">
<td><%= link_to trip.name, trip, class: 'underline hover:no-underline' %></td>
<td><%= trip.started_at.strftime("%d.%m.%Y, %H:%M") %></td>
<td><%= trip.ended_at.strftime("%d.%m.%Y, %H:%M") %></td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
</div>
</div>

View file

@ -0,0 +1,9 @@
<% content_for :title, 'New trip' %>
<div class="mx-auto md:w-2/3 w-full">
<h1 class="font-bold text-4xl">New trip</h1>
<%= render "form", trip: @trip %>
<%= link_to "Back to trips", trips_path, class: "ml-2 rounded-lg py-3 px-5 bg-gray-100 inline-block font-medium" %>
</div>

View file

@ -0,0 +1,64 @@
<% content_for :title, @trip.name %>
<div class="container mx-auto px-4 py-8 max-w-4xl">
<!-- Header Section -->
<div class="text-center mb-8">
<h1 class="text-4xl font-bold mb-2"><%= @trip.name %></h1>
<p class="text-lg text-base-content/60">Countries visited: [Placeholder]</p>
</div>
<!-- Map and Description Section -->
<div class="bg-base-100 mb-8">
<div class="p-6 grid grid-cols-1 md:grid-cols-2 gap-6">
<div class="w-full">
<div id="map" class="w-full h-96 bg-base-200">Map Placeholder</div>
</div>
<div class="w-full">
<div class="prose">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
<br>
<p>
At vero eos et accusam et justo duo dolores et ea rebum.
</p>
<br>
<p>
Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
</p>
</div>
<!-- Photos Grid Section -->
<% (1..12).each_slice(4) do |slice| %>
<div class="flex flex-row gap-4 mt-4 justify-center">
<% slice.each do %>
<div class="aspect-square rounded-box overflow-hidden bg-base-200 w-32">
<img src="https://placehold.co/128x128" alt="Photo Placeholder"
class="w-32 object-cover">
</div>
<% end %>
</div>
<% end %>
<div class="text-center mt-6">
<%= link_to "More photos on Immich", "#", class: "btn btn-primary" %>
</div>
</div>
</div>
</div>
<!-- Action Buttons Section -->
<div class="bg-base-100 items-center">
<div class="flex flex-wrap gap-2 justify-center">
<%= link_to "Edit this trip", edit_trip_path(@trip), class: "btn" %>
<%= link_to "Destroy this trip",
trip_path(@trip),
data: {
turbo_confirm: "Are you sure? This action will delete all points imported with this file",
turbo_method: :delete
},
class: "btn" %>
<%= link_to "Back to trips", trips_path, class: "btn" %>
</div>
</div>
</div>

View file

@ -3,6 +3,7 @@
require 'sidekiq/web'
Rails.application.routes.draw do
resources :trips
mount ActionCable.server => '/cable'
mount Rswag::Api::Engine => '/api-docs'
mount Rswag::Ui::Engine => '/api-docs'

View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
class CreateTrips < ActiveRecord::Migration[7.2]
def change
create_table :trips do |t|
t.string :name, null: false
t.datetime :started_at, null: false
t.datetime :ended_at, null: false
t.text :notes
t.references :user, null: false, foreign_key: true
t.timestamps
end
end
end

30
db/schema.rb generated
View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2024_10_30_152025) do
ActiveRecord::Schema[7.2].define(version: 2024_11_27_161621) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@ -175,6 +175,32 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_30_152025) do
t.index ["year"], name: "index_stats_on_year"
end
create_table "trips", force: :cascade do |t|
t.string "name", null: false
t.datetime "started_at", null: false
t.datetime "ended_at", null: false
t.text "notes"
t.bigint "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_trips_on_user_id"
end
create_table "user_digests", force: :cascade do |t|
t.bigint "user_id", null: false
t.integer "kind", default: 0, null: false
t.datetime "start_at", null: false
t.datetime "end_at"
t.integer "distance", default: 0, null: false
t.text "countries", default: [], array: true
t.text "cities", default: [], array: true
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["distance"], name: "index_user_digests_on_distance"
t.index ["kind"], name: "index_user_digests_on_kind"
t.index ["user_id"], name: "index_user_digests_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
@ -216,6 +242,8 @@ ActiveRecord::Schema[7.2].define(version: 2024_10_30_152025) do
add_foreign_key "points", "users"
add_foreign_key "points", "visits"
add_foreign_key "stats", "users"
add_foreign_key "trips", "users"
add_foreign_key "user_digests", "users"
add_foreign_key "visits", "areas"
add_foreign_key "visits", "places"
add_foreign_key "visits", "users"

9
spec/factories/trips.rb Normal file
View file

@ -0,0 +1,9 @@
FactoryBot.define do
factory :trip do
name { "MyString" }
started_at { "2024-11-27 17:16:21" }
ended_at { "2024-11-27 17:16:21" }
notes { "MyText" }
user { nil }
end
end

15
spec/models/trip_spec.rb Normal file
View file

@ -0,0 +1,15 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Trip, type: :model do
describe 'validations' do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_presence_of(:started_at) }
it { is_expected.to validate_presence_of(:ended_at) }
end
describe 'associations' do
it { is_expected.to belong_to(:user) }
end
end

View file

@ -13,6 +13,7 @@ RSpec.describe User, type: :model do
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) }
it { is_expected.to have_many(:trips).dependent(:destroy) }
end
describe 'callbacks' do

131
spec/requests/trips_spec.rb Normal file
View file

@ -0,0 +1,131 @@
require 'rails_helper'
# This spec was generated by rspec-rails when you ran the scaffold generator.
# It demonstrates how one might use RSpec to test the controller code that
# was generated by Rails when you ran the scaffold generator.
#
# It assumes that the implementation code is generated by the rails scaffold
# generator. If you are using any extension libraries to generate different
# controller code, this generated spec may or may not pass.
#
# It only uses APIs available in rails and/or rspec-rails. There are a number
# 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 "/trips", type: :request do
# This should return the minimal set of attributes required to create a valid
# Trip. As you add validations to Trip, be sure to
# adjust the attributes here as well.
let(:valid_attributes) {
skip("Add a hash of attributes valid for your model")
}
let(:invalid_attributes) {
skip("Add a hash of attributes invalid for your model")
}
describe "GET /index" do
it "renders a successful response" do
Trip.create! valid_attributes
get trips_url
expect(response).to be_successful
end
end
describe "GET /show" do
it "renders a successful response" do
trip = Trip.create! valid_attributes
get trip_url(trip)
expect(response).to be_successful
end
end
describe "GET /new" do
it "renders a successful response" do
get new_trip_url
expect(response).to be_successful
end
end
describe "GET /edit" do
it "renders a successful response" do
trip = Trip.create! valid_attributes
get edit_trip_url(trip)
expect(response).to be_successful
end
end
describe "POST /create" do
context "with valid parameters" do
it "creates a new Trip" do
expect {
post trips_url, params: { trip: valid_attributes }
}.to change(Trip, :count).by(1)
end
it "redirects to the created trip" do
post trips_url, params: { trip: valid_attributes }
expect(response).to redirect_to(trip_url(Trip.last))
end
end
context "with invalid parameters" do
it "does not create a new Trip" do
expect {
post trips_url, params: { trip: invalid_attributes }
}.to change(Trip, :count).by(0)
end
it "renders a response with 422 status (i.e. to display the 'new' template)" do
post trips_url, params: { trip: 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")
}
it "updates the requested trip" do
trip = Trip.create! valid_attributes
patch trip_url(trip), params: { trip: new_attributes }
trip.reload
skip("Add assertions for updated state")
end
it "redirects to the trip" do
trip = Trip.create! valid_attributes
patch trip_url(trip), params: { trip: new_attributes }
trip.reload
expect(response).to redirect_to(trip_url(trip))
end
end
context "with invalid parameters" do
it "renders a response with 422 status (i.e. to display the 'edit' template)" do
trip = Trip.create! valid_attributes
patch trip_url(trip), params: { trip: invalid_attributes }
expect(response).to have_http_status(:unprocessable_entity)
end
end
end
describe "DELETE /destroy" do
it "destroys the requested trip" do
trip = Trip.create! valid_attributes
expect {
delete trip_url(trip)
}.to change(Trip, :count).by(-1)
end
it "redirects to the trips list" do
trip = Trip.create! valid_attributes
delete trip_url(trip)
expect(response).to redirect_to(trips_url)
end
end
end