From 198bf3128af41129f1194e537266d28c8e659b55 Mon Sep 17 00:00:00 2001 From: Eugene Burmakin Date: Wed, 27 Nov 2024 20:14:17 +0100 Subject: [PATCH] Add trips model and scaffold controller --- app/controllers/trips_controller.rb | 51 +++++++++ app/helpers/trips_helper.rb | 2 + app/models/trip.rb | 7 ++ app/models/user.rb | 1 + app/views/shared/_navbar.html.erb | 2 + app/views/trips/_form.html.erb | 39 +++++++ app/views/trips/_trip.html.erb | 2 + app/views/trips/edit.html.erb | 8 ++ app/views/trips/index.html.erb | 48 ++++++++ app/views/trips/new.html.erb | 9 ++ app/views/trips/show.html.erb | 64 +++++++++++ config/routes.rb | 1 + db/migrate/20241127161621_create_trips.rb | 15 +++ db/schema.rb | 30 ++++- spec/factories/trips.rb | 9 ++ spec/models/trip_spec.rb | 15 +++ spec/models/user_spec.rb | 1 + spec/requests/trips_spec.rb | 131 ++++++++++++++++++++++ 18 files changed, 434 insertions(+), 1 deletion(-) create mode 100644 app/controllers/trips_controller.rb create mode 100644 app/helpers/trips_helper.rb create mode 100644 app/models/trip.rb create mode 100644 app/views/trips/_form.html.erb create mode 100644 app/views/trips/_trip.html.erb create mode 100644 app/views/trips/edit.html.erb create mode 100644 app/views/trips/index.html.erb create mode 100644 app/views/trips/new.html.erb create mode 100644 app/views/trips/show.html.erb create mode 100644 db/migrate/20241127161621_create_trips.rb create mode 100644 spec/factories/trips.rb create mode 100644 spec/models/trip_spec.rb create mode 100644 spec/requests/trips_spec.rb diff --git a/app/controllers/trips_controller.rb b/app/controllers/trips_controller.rb new file mode 100644 index 00000000..c576e2e8 --- /dev/null +++ b/app/controllers/trips_controller.rb @@ -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 diff --git a/app/helpers/trips_helper.rb b/app/helpers/trips_helper.rb new file mode 100644 index 00000000..04f333d4 --- /dev/null +++ b/app/helpers/trips_helper.rb @@ -0,0 +1,2 @@ +module TripsHelper +end diff --git a/app/models/trip.rb b/app/models/trip.rb new file mode 100644 index 00000000..572a90a4 --- /dev/null +++ b/app/models/trip.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class Trip < ApplicationRecord + belongs_to :user + + validates :name, :started_at, :ended_at, presence: true +end diff --git a/app/models/user.rb b/app/models/user.rb index 806b3b2f..a102d0b5 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -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 diff --git a/app/views/shared/_navbar.html.erb b/app/views/shared/_navbar.html.erb index 6ba5ead7..edd241de 100644 --- a/app/views/shared/_navbar.html.erb +++ b/app/views/shared/_navbar.html.erb @@ -10,6 +10,7 @@
  • <%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %>
  • <%= link_to 'Visitsβ'.html_safe, visits_url(status: :confirmed), class: "#{active_class?(visits_url)}" %>
  • <%= link_to 'Placesβ'.html_safe, places_url, class: "#{active_class?(places_url)}" %>
  • +
  • <%= link_to 'Tripsβ'.html_safe, trips_url, class: "#{active_class?(trips_url)}" %>
  • <%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %>
  • <%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %>
  • @@ -46,6 +47,7 @@
  • <%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %>
  • <%= link_to 'Visitsβ'.html_safe, visits_url(status: :confirmed), class: "#{active_class?(visits_url)}" %>
  • <%= link_to 'Placesβ'.html_safe, places_url, class: "#{active_class?(places_url)}" %>
  • +
  • <%= link_to 'Trips', trips_url, class: "#{active_class?(trips_url)}" %>
  • <%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %>
  • <%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %>
  • diff --git a/app/views/trips/_form.html.erb b/app/views/trips/_form.html.erb new file mode 100644 index 00000000..877c3068 --- /dev/null +++ b/app/views/trips/_form.html.erb @@ -0,0 +1,39 @@ +<%= form_with(model: trip, class: "contents") do |form| %> + <% if trip.errors.any? %> +
    +

    <%= pluralize(trip.errors.count, "error") %> prohibited this trip from being saved:

    + + +
    + <% end %> + +
    +
    + <%= form.label :name %> + <%= form.text_field :name, class: 'input input-bordered' %> +
    +
    +
    + <%= form.label :started_at %> + <%= form.datetime_field :started_at, include_seconds: false, class: 'input input-bordered', value: trip.started_at %> +
    +
    + <%= form.label :ended_at %> + <%= form.datetime_field :ended_at, include_seconds: false, class: 'input input-bordered', value: trip.ended_at %> +
    +
    +
    + +
    + <%= form.label :notes %> + <%= form.text_area :notes, class: 'textarea textarea-bordered w-full', rows: 10 %> +
    + +
    + <%= form.submit class: "rounded-lg py-3 px-5 bg-blue-600 text-white inline-block font-medium cursor-pointer" %> +
    +<% end %> diff --git a/app/views/trips/_trip.html.erb b/app/views/trips/_trip.html.erb new file mode 100644 index 00000000..87b36e7f --- /dev/null +++ b/app/views/trips/_trip.html.erb @@ -0,0 +1,2 @@ +
    +
    diff --git a/app/views/trips/edit.html.erb b/app/views/trips/edit.html.erb new file mode 100644 index 00000000..c55fbe26 --- /dev/null +++ b/app/views/trips/edit.html.erb @@ -0,0 +1,8 @@ +
    +

    Editing trip

    + + <%= 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" %> +
    diff --git a/app/views/trips/index.html.erb b/app/views/trips/index.html.erb new file mode 100644 index 00000000..aea0bb6a --- /dev/null +++ b/app/views/trips/index.html.erb @@ -0,0 +1,48 @@ +<% content_for :title, 'Trips' %> + +
    +
    + <% if @trips.empty? %> +
    +
    +
    +

    Hello there!

    +

    + Here you'll find your trips, but now there are none. Let's <%= link_to 'create one', new_trip_path, class: 'link' %>! +

    +
    +
    +
    + <% else %> +
    +
    + <%= paginate @trips %> +
    +
    +
    + + + + + + + + + + <% @trips.each do |trip| %> + + + + + + <% end %> + +
    NameStarted atEnded at
    <%= link_to trip.name, trip, class: 'underline hover:no-underline' %><%= trip.started_at.strftime("%d.%m.%Y, %H:%M") %><%= trip.ended_at.strftime("%d.%m.%Y, %H:%M") %>
    +
    + <% end %> +
    +
    diff --git a/app/views/trips/new.html.erb b/app/views/trips/new.html.erb new file mode 100644 index 00000000..cd64bfac --- /dev/null +++ b/app/views/trips/new.html.erb @@ -0,0 +1,9 @@ +<% content_for :title, 'New trip' %> + +
    +

    New trip

    + + <%= 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" %> +
    diff --git a/app/views/trips/show.html.erb b/app/views/trips/show.html.erb new file mode 100644 index 00000000..2b6414e8 --- /dev/null +++ b/app/views/trips/show.html.erb @@ -0,0 +1,64 @@ +<% content_for :title, @trip.name %> + +
    + +
    +

    <%= @trip.name %>

    +

    Countries visited: [Placeholder]

    +
    + + +
    +
    +
    +
    Map Placeholder
    +
    +
    +
    +

    + 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. +

    +
    +

    + 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. +

    +
    + + + <% (1..12).each_slice(4) do |slice| %> +
    + <% slice.each do %> +
    + Photo Placeholder +
    + <% end %> +
    + <% end %> + +
    + <%= link_to "More photos on Immich", "#", class: "btn btn-primary" %> +
    +
    +
    +
    + + +
    +
    + <%= 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" %> +
    +
    +
    diff --git a/config/routes.rb b/config/routes.rb index 430ba885..78474586 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -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' diff --git a/db/migrate/20241127161621_create_trips.rb b/db/migrate/20241127161621_create_trips.rb new file mode 100644 index 00000000..500b32bb --- /dev/null +++ b/db/migrate/20241127161621_create_trips.rb @@ -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 diff --git a/db/schema.rb b/db/schema.rb index 46ceb3d4..a48f37ac 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -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" diff --git a/spec/factories/trips.rb b/spec/factories/trips.rb new file mode 100644 index 00000000..dbecdc41 --- /dev/null +++ b/spec/factories/trips.rb @@ -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 diff --git a/spec/models/trip_spec.rb b/spec/models/trip_spec.rb new file mode 100644 index 00000000..55660a6a --- /dev/null +++ b/spec/models/trip_spec.rb @@ -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 diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 25421848..c42d969f 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -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 diff --git a/spec/requests/trips_spec.rb b/spec/requests/trips_spec.rb new file mode 100644 index 00000000..e0409075 --- /dev/null +++ b/spec/requests/trips_spec.rb @@ -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