diff --git a/.rubocop.yml b/.rubocop.yml index 5f6a612..f9fac95 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -10,3 +10,5 @@ Metrics/ClassLength: Max: 250 AllCops: NewCops: enable +Documentation: + Enabled: false \ No newline at end of file diff --git a/app/assets/stylesheets/room_bookings.css b/app/assets/stylesheets/room_bookings.css new file mode 100644 index 0000000..e2d9e95 --- /dev/null +++ b/app/assets/stylesheets/room_bookings.css @@ -0,0 +1,4 @@ +.is-hover { + --bs-bg-opacity: 0.1; + background-color: rgba(var(--bs-black-rgb), var(--bs-bg-opacity)) !important; +} \ No newline at end of file diff --git a/app/controllers/courses_controller.rb b/app/controllers/courses_controller.rb index 0b9facf..70b91ea 100644 --- a/app/controllers/courses_controller.rb +++ b/app/controllers/courses_controller.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Courses Controller class CoursesController < ApplicationController helper_method :sort_column, :sort_direction before_action :set_schedule, only: [:index] diff --git a/app/controllers/room_bookings_controller.rb b/app/controllers/room_bookings_controller.rb new file mode 100644 index 0000000..2fadc82 --- /dev/null +++ b/app/controllers/room_bookings_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# Room Bookings Controller +class RoomBookingsController < ApplicationController + def index + schedule_id = params[:schedule_id] + @schedule = Schedule.find(params[:schedule_id]) + @rooms = @schedule.rooms.where(is_active: true).where.not(building_code: 'ONLINE') + @tabs = TimeSlot.distinct.pluck(:day) + @active_tab = params[:active_tab] || @tabs[0] + @time_slots = TimeSlot.where(time_slots: { day: @active_tab }).to_a + @time_slots.sort_by! { |ts| Time.parse(ts.start_time) } + + # Fetch room bookings only for the specified schedule + @room_bookings = RoomBooking.joins(:room, :time_slot) + .where(rooms: { schedule_id: schedule_id }, time_slots: { day: @active_tab }) + + # Organize room bookings in a hash with room_id and time_slot_id as keys + @bookings_matrix = @room_bookings.each_with_object({}) do |booking, hash| + hash[[booking.room_id, booking.time_slot_id]] = booking + end + end +end diff --git a/app/controllers/time_slots_controller.rb b/app/controllers/time_slots_controller.rb index 5f19249..14d49db 100644 --- a/app/controllers/time_slots_controller.rb +++ b/app/controllers/time_slots_controller.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +# Time Slot Controller class TimeSlotsController < ApplicationController def filter @schedule = Schedule.find(params[:schedule_id]) diff --git a/app/helpers/courses_helper.rb b/app/helpers/courses_helper.rb index 46830e8..6fe3c67 100644 --- a/app/helpers/courses_helper.rb +++ b/app/helpers/courses_helper.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# Courses Helper module CoursesHelper end diff --git a/app/helpers/room_bookings_helper.rb b/app/helpers/room_bookings_helper.rb new file mode 100644 index 0000000..75f8f52 --- /dev/null +++ b/app/helpers/room_bookings_helper.rb @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# Room Bookings Helper +module RoomBookingsHelper +end diff --git a/app/helpers/time_slots_helper.rb b/app/helpers/time_slots_helper.rb index 7e0ff7b..c213472 100644 --- a/app/helpers/time_slots_helper.rb +++ b/app/helpers/time_slots_helper.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +# Time Slot Helper module TimeSlotsHelper end diff --git a/app/javascript/room_bookings.js b/app/javascript/room_bookings.js new file mode 100644 index 0000000..543cadb --- /dev/null +++ b/app/javascript/room_bookings.js @@ -0,0 +1,16 @@ +document.addEventListener("DOMContentLoaded", function () { + const tds = document.querySelectorAll(".grid-view-matrix td"); + + tds.forEach((td) => { + const table = td.closest("table"); + const colIndex = td.cellIndex; + const cols = table.querySelectorAll(`td:nth-child(${colIndex + 1})`); + + td.addEventListener("mouseover", () => { + cols.forEach((hover) => hover.classList.add("is-hover")); + }); + td.addEventListener("mouseleave", () => { + cols.forEach((hover) => hover.classList.remove("is-hover")); + }); + }); +}) \ No newline at end of file diff --git a/app/models/room.rb b/app/models/room.rb index ff9d80c..fba3313 100644 --- a/app/models/room.rb +++ b/app/models/room.rb @@ -3,6 +3,7 @@ # Room Model class Room < ApplicationRecord belongs_to :schedule + has_many :room_bookings, dependent: :destroy enum :campus, { NONE: 0, CS: 1, GV: 2 } validates :building_code, presence: true diff --git a/app/models/room_booking.rb b/app/models/room_booking.rb new file mode 100644 index 0000000..850e9a8 --- /dev/null +++ b/app/models/room_booking.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +class RoomBooking < ApplicationRecord + belongs_to :room + belongs_to :time_slot +end diff --git a/app/models/time_slot.rb b/app/models/time_slot.rb index d48c8dc..2887c4c 100644 --- a/app/models/time_slot.rb +++ b/app/models/time_slot.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class TimeSlot < ApplicationRecord + has_many :room_bookings, dependent: :destroy + validates :day, presence: true validates :start_time, presence: true validates :end_time, presence: true diff --git a/app/services/csv_handler.rb b/app/services/csv_handler.rb index 5871c5c..ba796ef 100644 --- a/app/services/csv_handler.rb +++ b/app/services/csv_handler.rb @@ -25,7 +25,6 @@ def parse_room_csv(schedule_id) ) end end - # Flash data to be received by controller { notice: 'Rooms successfully uploaded.' } rescue StandardError => e diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 3a2bbb0..01d75c5 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -17,6 +17,7 @@ <%= stylesheet_link_tag "https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css", integrity: "sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65", crossorigin: "anonymous" %> <%= stylesheet_link_tag "application", "data-turbo-track": "reload" %> <%= javascript_importmap_tags %> + <%= javascript_include_tag 'room_bookings' %> diff --git a/app/views/room_bookings/index.html.erb b/app/views/room_bookings/index.html.erb new file mode 100644 index 0000000..7b7b254 --- /dev/null +++ b/app/views/room_bookings/index.html.erb @@ -0,0 +1,64 @@ +
+
+ > +
+ <%= @schedule.schedule_name %> +
+
+
+ <%= link_to 'View Data', schedule_rooms_path(@schedule.id), class: 'col-5 btn btn-secondary px-4 mx-1' %> + <% if !@rooms.empty? %> + + <% end %> +
+
+
+<% if !@rooms.empty? %> + +
+ + + + + <% @rooms.each do |room| %> + + + <% end %> + + + + <% @time_slots.each do |time_slot| %> + + + + <% @rooms.each do |room| %> + <% booking = @bookings_matrix[[room.id, time_slot.id]] %> + + + <% end %> + + <% end %> + +
Times \ Rooms<%= room.building_code %> <%= room.room_number %>
<%= time_slot.start_time %> - <%= time_slot.end_time %> + <% if booking %> + <%= booking.is_available ? 'Available' : 'Booked' %> | + <%= booking.is_lab ? 'Lab' : 'Lecture' %> + <% else %> + <% end %> +
+
+<% else %> +

No rooms added to this schedule, click on View Data to Add Rooms!

+<% end %> \ No newline at end of file diff --git a/app/views/shared/_insched_nav.html.erb b/app/views/shared/_insched_nav.html.erb index fca31fa..f616970 100644 --- a/app/views/shared/_insched_nav.html.erb +++ b/app/views/shared/_insched_nav.html.erb @@ -6,9 +6,7 @@
- + <%= link_to 'Add Predefined Courses', schedule_room_bookings_path(@schedule), class: 'col btn btn-secondary px-4 mx-1' %> diff --git a/config/routes.rb b/config/routes.rb index 64608f4..c5aa2a4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -34,13 +34,13 @@ resources :instructors, only: [:index] post :upload_instructors, on: :member - resources :courses, only: [:index] post :upload_courses, on: :member post 'time_slots', to: 'time_slots#filter', as: 'filter_time_slots' get '/time_slots', to: 'time_slots#index' + resources :room_bookings, only: [:index] end # Show Time Slot View diff --git a/db/migrate/20241027021034_create_room_bookings.rb b/db/migrate/20241027021034_create_room_bookings.rb new file mode 100644 index 0000000..5e5164c --- /dev/null +++ b/db/migrate/20241027021034_create_room_bookings.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +class CreateRoomBookings < ActiveRecord::Migration[7.2] + def change + create_table :room_bookings do |t| + t.references :room, null: false, foreign_key: true + t.references :time_slot, null: false, foreign_key: true + t.boolean :is_available + t.boolean :is_lab + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 8ebee1f..1314eff 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,9 @@ # migrations from scratch. Old migrations may fail to apply correctly if those # migrations use external dependencies or application code. # -# It's strongly recommended that you check this file into your version control syste +# It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 20_241_023_233_115) do +ActiveRecord::Schema[7.2].define(version: 20_241_027_021_034) do create_table 'courses', force: :cascade do |t| t.string 'course_number' t.integer 'max_seats' @@ -24,7 +24,6 @@ t.index ['schedule_id'], name: 'index_courses_on_schedule_id' end - create_table 'instructors', force: :cascade do |t| t.integer 'id_number' t.string 'last_name' @@ -40,6 +39,17 @@ t.index ['schedule_id'], name: 'index_instructors_on_schedule_id' end + create_table 'room_bookings', force: :cascade do |t| + t.integer 'room_id', null: false + t.integer 'time_slot_id', null: false + t.boolean 'is_available' + t.boolean 'is_lab' + t.datetime 'created_at', null: false + t.datetime 'updated_at', null: false + t.index ['room_id'], name: 'index_room_bookings_on_room_id' + t.index ['time_slot_id'], name: 'index_room_bookings_on_time_slot_id' + end + create_table 'rooms', force: :cascade do |t| t.integer 'campus' t.boolean 'is_lecture_hall' @@ -70,13 +80,8 @@ t.datetime 'created_at', null: false t.datetime 'updated_at', null: false t.index ['course_id'], name: 'index_sections_on_course_id' - end - - - - create_table 'time_slots', force: :cascade do |t| t.string 'day' t.string 'start_time' @@ -97,10 +102,10 @@ t.index ['email'], name: 'index_users_on_email', unique: true end - add_foreign_key 'courses', 'schedules' add_foreign_key 'instructors', 'schedules' + add_foreign_key 'room_bookings', 'rooms' + add_foreign_key 'room_bookings', 'time_slots' add_foreign_key 'rooms', 'schedules' add_foreign_key 'sections', 'courses' - end diff --git a/db/seeds.rb b/db/seeds.rb index 058a583..b81c98d 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -10,6 +10,8 @@ # MovieGenre.find_or_create_by!(name: genre_name) # end +TimeSlot.delete_all + TimeSlot.create([ { day: 'MWF', start_time: '8:00', end_time: '8:50', slot_type: 'LEC' }, { day: 'MWF', start_time: '9:10', end_time: '10:00', slot_type: 'LEC' }, diff --git a/features/room_bookings.feature b/features/room_bookings.feature new file mode 100644 index 0000000..ba8d905 --- /dev/null +++ b/features/room_bookings.feature @@ -0,0 +1,44 @@ +Feature: Rooms Page + Scenario: Checking a room booking + Given I am logged in as a user with first name "Test" + And a schedule exists with the schedule name "Sched 1" and semester name "Fall 2024" + And the following rooms exist for that schedule: + | campus | building_code | room_number | capacity | is_active | is_lab | is_learning_studio | is_lecture_hall | + | CS | BLDG1 | 101 | 30 | true | true | true | true | + | GV | BLDG2 | 102 | 50 | true | true | true | true | + | CS | BLDG3 | 102 | 50 | false | true | true | true | + And the following time slots exist for that schedule: + | day | start_time | end_time | slot_type | + | MWF | 09:00 | 10:00 | "LEC" | + | MW | 08:00 | 10:00 | "LEC" | + When I visit the room bookings page for "Sched 1" + Then I should see "View Data" + And I should see "09:00 - 10:00" + And I should see "BLDG1 101" + + Scenario: Checking a room booking and changing tab + Given I am logged in as a user with first name "Test" + And a schedule exists with the schedule name "Sched 1" and semester name "Fall 2024" + And the following rooms exist for that schedule: + | campus | building_code | room_number | capacity | is_active | is_lab | is_learning_studio | is_lecture_hall | + | CS | BLDG1 | 101 | 30 | true | true | true | true | + | GV | BLDG2 | 102 | 50 | true | true | true | true | + | CS | BLDG3 | 102 | 50 | false | true | true | true | + And the following time slots exist for that schedule: + | day | start_time | end_time | slot_type | + | MWF | 09:00 | 10:00 | "LEC" | + | MW | 08:00 | 10:00 | "LEC" | + When I visit the room bookings page for "Sched 1" + And I click "MW" + Then I should see "View Data" + And I should see "08:00 - 10:00" + And I should not see "09:00 - 10:00" + And I should see "BLDG1 101" + + Scenario: Checking empty room bookings + Given I am logged in as a user with first name "Test" + And a schedule exists with the schedule name "Sched 1" and semester name "Fall 2024" + When I visit the room bookings page for "Sched 1" + Then I should see "View Data" + And I should not see "Generate Remaining" + And I should see "No rooms added to this schedule, click on View Data to Add Rooms!" \ No newline at end of file diff --git a/features/step_definitions/room_bookings_steps.rb b/features/step_definitions/room_bookings_steps.rb new file mode 100644 index 0000000..d14318e --- /dev/null +++ b/features/step_definitions/room_bookings_steps.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +Given(/I am on the room bookings page for "(.*)"/) do |schedule_name| + @schedule = Schedule.find(schedule_name: schedule_name) + visit schedule_room_bookings_path(@schedule) +end + +When(/^I visit the room bookings page for "(.*)"$/) do |schedule_name| + @schedule = Schedule.where(schedule_name: schedule_name)[0] + visit schedule_room_bookings_path(schedule_id: @schedule.id) +end + +Given('the following time slots exist for that schedule:') do |table| + table.hashes.each do |time_slot| + TimeSlot.create!( + day: time_slot['day'], + start_time: time_slot['start_time'], + end_time: time_slot['end_time'], + slot_type: time_slot['slot_type'] + ) + end +end diff --git a/spec/controllers/courses_controller_spec.rb b/spec/controllers/courses_controller_spec.rb index 244a9f9..6e32bfe 100644 --- a/spec/controllers/courses_controller_spec.rb +++ b/spec/controllers/courses_controller_spec.rb @@ -60,7 +60,6 @@ let!(:section31) do create(:section, section_number: '500', seats_alloted: 150, course_id: course3.id) end - context 'without any sorting' do it 'assigns all courses to @courses' do @@ -69,15 +68,14 @@ end end - context 'with sorting by course_number' do it 'assigns courses sorted by course_number ascending to @courses' do get :index, params: { schedule_id: schedule.id, sort: 'course_number', direction: 'asc' } expect(assigns(:courses)).to eq([course1, course2, course3]) end it 'assigns courses sorted by course_number descending to @courses' do - get :index, params: { schedule_id: schedule.id, sort: 'course_number', direction: 'desc' } - expect(assigns(:courses)).to eq([course3, course2, course1]) + get :index, params: { schedule_id: schedule.id, sort: 'course_number', direction: 'desc' } + expect(assigns(:courses)).to eq([course3, course2, course1]) end end @@ -87,19 +85,19 @@ expect(assigns(:courses)).to eq([course1, course2, course3]) end it 'assigns courses sorted by max_seats descending to @courses' do - get :index, params: { schedule_id: schedule.id, sort: 'max_seats', direction: 'desc' } - expect(assigns(:courses)).to eq([course3, course2, course1]) + get :index, params: { schedule_id: schedule.id, sort: 'max_seats', direction: 'desc' } + expect(assigns(:courses)).to eq([course3, course2, course1]) end end - + context 'with sorting by lecture_type' do it 'assigns courses sorted by lecture_type ascending to @courses' do get :index, params: { schedule_id: schedule.id, sort: 'lecture_type', direction: 'asc' } expect(assigns(:courses)).to eq([course3, course1, course2]) end it 'assigns courses sorted by lecture_type descending to @courses' do - get :index, params: { schedule_id: schedule.id, sort: 'lecture_type', direction: 'desc' } - expect(assigns(:courses)).to eq([course2, course1, course3]) + get :index, params: { schedule_id: schedule.id, sort: 'lecture_type', direction: 'desc' } + expect(assigns(:courses)).to eq([course2, course1, course3]) end end @@ -109,8 +107,8 @@ expect(assigns(:courses)).to eq([course3, course2, course1]) end it 'assigns courses sorted by num_labs descending to @courses' do - get :index, params: { schedule_id: schedule.id, sort: 'num_labs', direction: 'desc' } - expect(assigns(:courses)).to eq([course1, course2, course3]) + get :index, params: { schedule_id: schedule.id, sort: 'num_labs', direction: 'desc' } + expect(assigns(:courses)).to eq([course1, course2, course3]) end end end diff --git a/spec/controllers/room_bookings_controller_spec.rb b/spec/controllers/room_bookings_controller_spec.rb new file mode 100644 index 0000000..cec0ca3 --- /dev/null +++ b/spec/controllers/room_bookings_controller_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +# spec/controllers/room_bookings_controller_spec.rb +require 'rails_helper' + +RSpec.describe RoomBookingsController, type: :controller do + let!(:schedule) { create(:schedule) } + let!(:room1) { create(:room, schedule: schedule) } + let!(:room2) { create(:room, schedule: schedule) } + let!(:time_slot1) { create(:time_slot, day: 'Monday', start_time: '09:00', end_time: '10:00') } + let!(:time_slot2) { create(:time_slot, day: 'Monday', start_time: '10:00', end_time: '11:00') } + let!(:room_booking1) { create(:room_booking, room: room1, time_slot: time_slot1, is_available: true) } + let!(:room_booking2) { create(:room_booking, room: room2, time_slot: time_slot2, is_available: false) } + + before do + @user = User.create!(uid: '12345', provider: 'google_oauth2', email: 'test@example.com', first_name: 'John', + last_name: 'Doe') + allow(controller).to receive(:logged_in?).and_return(true) + controller.instance_variable_set(:@current_user, @user) + end + + describe 'GET #index' do + context 'when room_booking already exists' do + before do + get :index, params: { schedule_id: schedule.id } + end + + it 'returns a successful response' do + expect(response).to have_http_status(:success) + end + + it 'assigns @rooms' do + expect(assigns(:rooms)).to match_array([room1, room2]) + end + + it 'assigns @time_slots' do + expect(assigns(:time_slots)).to match_array([time_slot1, time_slot2]) + end + + it 'assigns @bookings_matrix with room_booking data' do + bookings_matrix = assigns(:bookings_matrix) + expect(bookings_matrix[[room1.id, time_slot1.id]]).to eq(room_booking1) + expect(bookings_matrix[[room2.id, time_slot2.id]]).to eq(room_booking2) + end + + it 'renders the index template' do + expect(response).to render_template(:index) + end + end + end +end diff --git a/spec/controllers/schedules_controller_spec.rb b/spec/controllers/schedules_controller_spec.rb index d0db2f9..f04c322 100644 --- a/spec/controllers/schedules_controller_spec.rb +++ b/spec/controllers/schedules_controller_spec.rb @@ -130,6 +130,7 @@ end end end + describe 'POST #upload_instructors' do let(:file_valid) { fixture_file_upload(Rails.root.join('spec/fixtures/instructors/instructors_valid.csv'), 'text/csv') } let(:file_invalid) { fixture_file_upload(Rails.root.join('spec/fixtures/rooms/rooms_invalid.csv'), 'text/csv') } @@ -162,4 +163,37 @@ end end end + + describe 'POST #upload_courses' do + let(:file_valid) { fixture_file_upload(Rails.root.join('spec/fixtures/courses/Course_list_valid.csv'), 'text/csv') } + let(:file_invalid) { fixture_file_upload(Rails.root.join('spec/fixtures/courses/Course_list_invalid.csv'), 'text/csv') } + let!(:schedule1) { create(:schedule) } + + context 'with a valid CSV file' do + it "processes the CSV file, sets a success flash, and redirects to the user's page" do + post :upload_courses, params: { id: schedule1.id, course_file: file_valid } + + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(schedule_path(schedule1)) # Redirect to the current user's page + expect(flash[:notice]).to eq('Courses successfully uploaded.') + end + end + + context 'when invalid CSV file is selected' do + it "sets an error flash and redirects to the user's page" do + post :upload_courses, params: { id: schedule1.id, course_file: file_invalid } + expect(flash[:alert]).to include('Missing required headers:') + end + end + + context 'when no CSV file is selected' do + it "sets an error flash and redirects to the user's page" do + post :upload_courses, params: { id: schedule1.id, room_file: nil } + + expect(response).to have_http_status(:redirect) + expect(response).to redirect_to(schedule_path(schedule1)) # Redirect to the current user's page + expect(flash[:alert]).to eq('Please upload a CSV file.') + end + end + end end diff --git a/spec/factories.rb b/spec/factories.rb index 6be7ba9..60d7914 100644 --- a/spec/factories.rb +++ b/spec/factories.rb @@ -16,6 +16,7 @@ is_learning_studio { false } is_lecture_hall { false } comments { 'A large lecture hall.' } + association :schedule end factory :instructor do @@ -29,4 +30,17 @@ beaware_of { 'Some notes or warnings.' } # Default text association :schedule # Associate with a Schedule, assuming you have a Schedule factory as well end + + factory :room_booking do + association :room + association :time_slot + is_available { true } + end + + factory :time_slot do + day { 'Monday' } + start_time { '09:00' } + end_time { '10:00' } + slot_type { 'Lecture' } + end end diff --git a/test/controllers/room_bookings_controller_test.rb b/test/controllers/room_bookings_controller_test.rb new file mode 100644 index 0000000..b579650 --- /dev/null +++ b/test/controllers/room_bookings_controller_test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'test_helper' + +class RoomBookingsControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/factories/room_bookings.rb b/test/factories/room_bookings.rb new file mode 100644 index 0000000..5ea3109 --- /dev/null +++ b/test/factories/room_bookings.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +FactoryBot.define do + # factory :room_booking do + # room { nil } + # time_slot { nil } + # is_available { false } + # is_lab { false } + # end +end diff --git a/test/models/room_booking_test.rb b/test/models/room_booking_test.rb new file mode 100644 index 0000000..cb1a846 --- /dev/null +++ b/test/models/room_booking_test.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +require 'test_helper' + +class RoomBookingTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end