diff --git a/app/controllers/room_bookings_controller.rb b/app/controllers/room_bookings_controller.rb index 533c36f..6c51f26 100644 --- a/app/controllers/room_bookings_controller.rb +++ b/app/controllers/room_bookings_controller.rb @@ -1,23 +1,60 @@ # frozen_string_literal: true -# Room Bookings Controller class RoomBookingsController < ApplicationController + before_action :set_schedule + 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) } + @time_slots = TimeSlot.where(day: @active_tab).order(:start_time) - # Fetch room bookings only for the specified schedule @room_bookings = RoomBooking.joins(:room, :time_slot) - .where(rooms: { schedule_id: }, time_slots: { day: @active_tab }) - - # Organize room bookings in a hash with room_id and time_slot_id as keys + .where(rooms: { schedule_id: @schedule.id }, time_slots: { day: @active_tab }) @bookings_matrix = @room_bookings.each_with_object({}) do |booking, hash| hash[[booking.room_id, booking.time_slot_id]] = booking end end + + before_action :set_schedule + + def toggle_availability + room_booking = RoomBooking.find_or_initialize_by(room_id: params[:room_id], time_slot_id: params[:time_slot_id]) + new_status = !room_booking.is_available + room_booking.update(is_available: new_status) + + overlapping_time_slots = find_overlapping_time_slots(room_booking.time_slot) + overlapping_time_slots.each do |overlapping_slot| + overlapping_booking = RoomBooking.find_or_initialize_by(room_id: params[:room_id], time_slot_id: overlapping_slot.id) + overlapping_booking.update(is_available: new_status) + end + + redirect_to schedule_room_bookings_path(@schedule, active_tab: params[:active_tab]) + end + + private + + def set_schedule + @schedule = Schedule.find(params[:schedule_id]) + end + + def find_overlapping_time_slots(time_slot) + relevant_days = calculate_relevant_days(time_slot.day) + + TimeSlot.where(day: relevant_days) + .where('start_time < ? AND end_time > ?', time_slot.end_time, time_slot.start_time) + end + + def calculate_relevant_days(current_day) + case current_day + when 'MWF' + %w[MWF MW F] + when 'MW' + %w[MWF MW] + when 'F' + %w[MWF F] + else + [current_day] + end + end end diff --git a/app/views/room_bookings/index.html.erb b/app/views/room_bookings/index.html.erb index 7b7b254..e25c6e6 100644 --- a/app/views/room_bookings/index.html.erb +++ b/app/views/room_bookings/index.html.erb @@ -33,32 +33,29 @@ Times \ Rooms <% @rooms.each do |room| %> - <%= room.building_code %> <%= room.room_number %> - + <%= room.building_code %> <%= room.room_number %> <% end %> - <% @time_slots.each do |time_slot| %> + <% @time_slots.each do |time_slot| %> - <%= time_slot.start_time %> - <%= time_slot.end_time %> - - <% @rooms.each do |room| %> + <%= time_slot.start_time %> - <%= time_slot.end_time %> + <% @rooms.each do |room| %> <% booking = @bookings_matrix[[room.id, time_slot.id]] %> - - <% if booking %> - <%= booking.is_available ? 'Available' : 'Booked' %> | - <%= booking.is_lab ? 'Lab' : 'Lecture' %> - <% else %> - <% end %> - + <% if booking && !booking.is_available %> + <%= button_to "U", toggle_availability_room_bookings_path, method: :post, params: { room_id: room.id, time_slot_id: time_slot.id, active_tab: @active_tab, schedule_id: @schedule.id }, class: "btn btn-sm btn-success" %> + <% else %> + <%= button_to "BL", toggle_availability_room_bookings_path, method: :post, params: { room_id: room.id, time_slot_id: time_slot.id, active_tab: @active_tab, schedule_id: @schedule.id }, class: "btn btn-sm btn-danger" %> <% end %> - + <% end %> + + <% end %> <% else %>

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

-<% end %> \ No newline at end of file +<% end %> diff --git a/config/routes.rb b/config/routes.rb index c5aa2a4..40f66ee 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -41,6 +41,11 @@ get '/time_slots', to: 'time_slots#index' resources :room_bookings, only: [:index] + post 'room_bookings/toggle_availability', to: 'room_bookings#toggle_availability' + end + + resources :room_bookings do + post 'toggle_availability', on: :collection end # Show Time Slot View diff --git a/db/schema.rb b/db/schema.rb index 19dfe95..bc0a95d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -12,113 +10,113 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[7.2].define(version: 20_241_029_040_851) do - create_table 'courses', force: :cascade do |t| - t.string 'course_number' - t.integer 'max_seats' - t.string 'lecture_type' - t.integer 'num_labs' - t.integer 'schedule_id', null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['schedule_id'], name: 'index_courses_on_schedule_id' +ActiveRecord::Schema[7.2].define(version: 2024_10_29_040851) do + create_table "courses", force: :cascade do |t| + t.string "course_number" + t.integer "max_seats" + t.string "lecture_type" + t.integer "num_labs" + t.integer "schedule_id", null: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["schedule_id"], name: "index_courses_on_schedule_id" end - create_table 'instructor_preferences', force: :cascade do |t| - t.integer 'instructor_id', null: false - t.integer 'preference_level' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.integer 'course_id', null: false - t.index ['course_id'], name: 'index_instructor_preferences_on_course_id' - t.index ['instructor_id'], name: 'index_instructor_preferences_on_instructor_id' + create_table "instructor_preferences", force: :cascade do |t| + t.integer "instructor_id", null: false + t.integer "preference_level" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "course_id", null: false + t.index ["course_id"], name: "index_instructor_preferences_on_course_id" + t.index ["instructor_id"], name: "index_instructor_preferences_on_instructor_id" end - create_table 'instructors', force: :cascade do |t| - t.integer 'id_number' - t.string 'last_name' - t.string 'first_name' - t.string 'middle_name' - t.string 'email' - t.boolean 'before_9' - t.boolean 'after_3' - t.text 'beaware_of' - t.integer 'schedule_id' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.integer 'max_course_load' - t.index ['schedule_id'], name: 'index_instructors_on_schedule_id' + create_table "instructors", force: :cascade do |t| + t.integer "id_number" + t.string "last_name" + t.string "first_name" + t.string "middle_name" + t.string "email" + t.boolean "before_9" + t.boolean "after_3" + t.text "beaware_of" + t.integer "schedule_id" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "max_course_load" + 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' + 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' - t.boolean 'is_learning_studio' - t.boolean 'is_lab' - t.string 'building_code' - t.string 'room_number' - t.integer 'capacity' - t.boolean 'is_active' - t.string 'comments' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.integer 'schedule_id', default: -1, null: false - t.index ['schedule_id'], name: 'index_rooms_on_schedule_id' + create_table "rooms", force: :cascade do |t| + t.integer "campus" + t.boolean "is_lecture_hall" + t.boolean "is_learning_studio" + t.boolean "is_lab" + t.string "building_code" + t.string "room_number" + t.integer "capacity" + t.boolean "is_active" + t.string "comments" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.integer "schedule_id", default: -1, null: false + t.index ["schedule_id"], name: "index_rooms_on_schedule_id" end - create_table 'schedules', force: :cascade do |t| - t.string 'schedule_name' - t.string 'semester_name' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false + create_table "schedules", force: :cascade do |t| + t.string "schedule_name" + t.string "semester_name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - create_table 'sections', force: :cascade do |t| - t.string 'section_number' - t.integer 'seats_alloted' - t.integer 'course_id', null: false - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.index ['course_id'], name: 'index_sections_on_course_id' + create_table "sections", force: :cascade do |t| + t.string "section_number" + t.integer "seats_alloted" + t.integer "course_id", null: false + 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' - t.string 'end_time' - t.string 'slot_type' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false + create_table "time_slots", force: :cascade do |t| + t.string "day" + t.string "start_time" + t.string "end_time" + t.string "slot_type" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false end - create_table 'users', force: :cascade do |t| - t.string 'email' - t.string 'first_name' - t.string 'last_name' - t.datetime 'created_at', null: false - t.datetime 'updated_at', null: false - t.string 'uid' - t.string 'provider' - t.index ['email'], name: 'index_users_on_email', unique: true + create_table "users", force: :cascade do |t| + t.string "email" + t.string "first_name" + t.string "last_name" + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.string "uid" + t.string "provider" + t.index ["email"], name: "index_users_on_email", unique: true end - add_foreign_key 'courses', 'schedules' - add_foreign_key 'instructor_preferences', 'courses' - add_foreign_key 'instructor_preferences', 'instructors' - 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' + add_foreign_key "courses", "schedules" + add_foreign_key "instructor_preferences", "courses" + add_foreign_key "instructor_preferences", "instructors" + 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/features/room_booking_management.feature b/features/room_booking_management.feature new file mode 100644 index 0000000..2091057 --- /dev/null +++ b/features/room_booking_management.feature @@ -0,0 +1,27 @@ +Feature: Room Booking Management + Scenario: Toggling room booking availability + 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: + | day | start_time | end_time | slot_type | + | MWF | 09:00 | 10:00 | "LEC" | + | MW | 09:00 | 10:00 | "LEC" | + | F | 09:00 | 10:00 | "LEC" | + + And a room booking exists for "Sched 1" with room "BLDG1" "101" at "09:00" that is available + When I visit the room bookings page for "Sched 1" + + And I toggle availability for the room booking in "BLDG1" "101" at "09:00" + Then the booking in "BLDG1" "101" at "09:00" "MWF" should be unavailable + + When I click "MW" + And I toggle unavailability for the room booking in "BLDG1" "101" at "09:00" + + Then the booking in "BLDG1" "101" at "09:00" "F" should be unavailable + And the booking in "BLDG1" "101" at "09:00" "MW" should be available + And the booking in "BLDG1" "101" at "09:00" "MWF" should be available \ No newline at end of file diff --git a/features/step_definitions/room_booking_management_steps.rb b/features/step_definitions/room_booking_management_steps.rb new file mode 100644 index 0000000..9d10936 --- /dev/null +++ b/features/step_definitions/room_booking_management_steps.rb @@ -0,0 +1,45 @@ + + +Given('a room booking exists for {string} with room {string} {string} at {string} that is available') do |schedule, building, room, start| + RoomBooking.create!( + room: Room.find_by(schedule_id: Schedule.find_by(schedule_name: schedule ),building_code: building, room_number: room), + time_slot: TimeSlot.find_by(start_time: start), + is_available: true, + is_lab: false + ) +end + +When('I toggle availability for the room booking in {string} {string} at {string}') do |building, room, start| + timeslot=TimeSlot.find_by(start_time: start) + row = find('tr', text: "#{timeslot.start_time} - #{timeslot.end_time}") + room_headers = all('th') + room_header_index = room_headers.find_index { |header| header.text == "#{building} #{room}" } + room_cell = row.all('td')[room_header_index-1] + room_cell.find_button('BL').click +end + +When('I toggle unavailability for the room booking in {string} {string} at {string}') do |building, room, start| + timeslot=TimeSlot.find_by(start_time: start) + row = find('tr', text: "#{timeslot.start_time} - #{timeslot.end_time}") + room_headers = all('th') + room_header_index = room_headers.find_index { |header| header.text == "#{building} #{room}" } + room_cell = row.all('td')[room_header_index-1] + room_cell.find_button('U').click +end + +Then('the booking in {string} {string} at {string} {string} should be unavailable') do | building, room, start, days| + expect(RoomBooking.find_by( + room: Room.find_by(schedule_id: @schedule.id, building_code: building, room_number: room), + time_slot: TimeSlot.find_by(start_time: start, day: days) + ).is_available).to eq(false) +end +Then('the booking in {string} {string} at {string} {string} should be available') do | building, room, start, days| + expect(RoomBooking.find_by( + room: Room.find_by(schedule_id: @schedule.id, building_code: building, room_number: room), + time_slot: TimeSlot.find_by(start_time: start, day: days) + ).is_available).to eq(true) +end + +Then('overlapping bookings should also be unavailable') do + pending # Write code here that turns the phrase above into concrete actions +end \ No newline at end of file diff --git a/spec/controllers/room_bookings_controller_spec.rb b/spec/controllers/room_bookings_controller_spec.rb index d2217cb..8baa82c 100644 --- a/spec/controllers/room_bookings_controller_spec.rb +++ b/spec/controllers/room_bookings_controller_spec.rb @@ -13,38 +13,100 @@ 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') + @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 + 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 - it 'returns a successful response' do - expect(response).to have_http_status(:success) + describe 'POST #toggle_availability' do + context 'when booking is currently available' do + it 'toggles availability to false' do + expect do + post :toggle_availability, params: { room_id: room1.id, time_slot_id: time_slot1.id, schedule_id: schedule.id } + room_booking1.reload + end.to change { room_booking1.is_available }.from(true).to(false) + + expect(response).to redirect_to(schedule_room_bookings_path(schedule, active_tab: nil)) end - it 'assigns @rooms' do - expect(assigns(:rooms)).to match_array([room1, room2]) + it 'toggles availability for overlapping bookings' do + overlapping_slot = create(:time_slot, day: 'Monday', start_time: '09:30', end_time: '10:30') + create(:room_booking, room: room1, time_slot: overlapping_slot, is_available: true) + + expect do + post :toggle_availability, params: { room_id: room1.id, time_slot_id: time_slot1.id, schedule_id: schedule.id } + end.to change { RoomBooking.find_by(room: room1, time_slot: overlapping_slot).is_available }.from(true).to(false) + + overlapping_booking = RoomBooking.find_by(room: room1, time_slot: overlapping_slot) + expect(overlapping_booking.is_available).to eq(false) + + expect(response).to redirect_to(schedule_room_bookings_path(schedule, active_tab: nil)) end + end + + context 'when booking is currently blocked' do + before { room_booking1.update(is_available: false) } - it 'assigns @time_slots' do - expect(assigns(:time_slots)).to match_array([time_slot1, time_slot2]) + it 'toggles availability to true' do + expect do + post :toggle_availability, params: { room_id: room1.id, time_slot_id: time_slot1.id, schedule_id: schedule.id } + room_booking1.reload + end.to change { room_booking1.is_available }.from(false).to(true) + + expect(response).to redirect_to(schedule_room_bookings_path(schedule, active_tab: nil)) 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) + it 'toggles availability for overlapping bookings' do + overlapping_slot = create(:time_slot, day: 'Monday', start_time: '09:30', end_time: '10:30') + create(:room_booking, room: room1, time_slot: overlapping_slot, is_available: false) + + expect do + post :toggle_availability, params: { room_id: room1.id, time_slot_id: time_slot1.id, schedule_id: schedule.id } + end.to change { RoomBooking.find_by(room: room1, time_slot: overlapping_slot).is_available }.from(false).to(true) + + overlapping_booking = RoomBooking.find_by(room: room1, time_slot: overlapping_slot) + expect(overlapping_booking.is_available).to eq(true) + + expect(response).to redirect_to(schedule_room_bookings_path(schedule, active_tab: nil)) end + end + end - it 'renders the index template' do - expect(response).to render_template(:index) + describe 'private methods' do + describe '#calculate_relevant_days' do + it 'returns the correct relevant days' do + expect(controller.send(:calculate_relevant_days, 'MWF')).to match_array(%w[MWF MW F]) + expect(controller.send(:calculate_relevant_days, 'MW')).to match_array(%w[MWF MW]) + expect(controller.send(:calculate_relevant_days, 'F')).to match_array(%w[MWF F]) + expect(controller.send(:calculate_relevant_days, 'TR')).to match_array(['TR']) end end end