diff --git a/app/assets/javascripts/osem-schedule.js b/app/assets/javascripts/osem-schedule.js index baae1933..faeb6bcf 100644 --- a/app/assets/javascripts/osem-schedule.js +++ b/app/assets/javascripts/osem-schedule.js @@ -1,79 +1,126 @@ -var url; // Should be initialize in Schedule.initialize +var conference_id; // Should be initialize in Schedule.initialize var schedule_id; // Should be initialize in Schedule.initialize -function showError(error){ - // Delete other error messages before showing the new one +var events_to_create = {}; +var events_to_update = {}; +var events_to_remove = {}; + +function showMessage(message, type){ + // Delete other messages before showing the new one $('.unobtrusive-flash-container').empty(); - UnobtrusiveFlash.showFlashMessage(error, {type: 'error'}); + UnobtrusiveFlash.showFlashMessage(message, {type: type}); } var Schedule = { - initialize: function(url_param, schedule_id_param) { - url = url_param; + initialize: function(conference_id_param, schedule_id_param) { + conference_id = conference_id_param; schedule_id = schedule_id_param; }, + invalidateCells: function(element, length) { + for (i = 0; i < length; i++) { + element.addClass('with-event'); + element = element.next(); + } + }, + validateCells: function(element, length) { + for (i = 0; i < length; i++) { + element.removeClass('with-event'); + element = element.next(); + } + }, remove: function(element) { var e = $("#" + element); + var event_id = e.attr("event_id"); var event_schedule_id = e.attr("event_schedule_id"); if(event_schedule_id != null){ - var my_url = url + '/' + event_schedule_id; - var success_callback = function(data) { - console.log(data); - e.attr("event_schedule_id", null); - e.appendTo($(".unscheduled-events")); - e.find(".schedule-event-delete-button").hide(); - } - var error_callback = function(data) { - console.log(data); - showError($.parseJSON(data.responseText).errors); + events_to_remove[event_id] = event_schedule_id; + } + delete events_to_create[event_id]; + delete events_to_update[event_id]; + Schedule.validateCells(e.parent(), e.attr("length")); + var unscheduled = $(".unscheduled-events"); + e.appendTo(unscheduled); + e.find(".schedule-event-delete-button").hide(); + }, + add: function (new_parent, event) { + var event_id = event.attr("event_id"); + var event_schedule_id = event.attr("event_schedule_id"); + var room_id = new_parent.attr("room_id"); + var start_time = (new_parent.attr("date") + ' ' + new_parent.attr("hour")); + if(event_schedule_id != null){ + var params = {}; + params[event_schedule_id] = { + room_id: room_id, + start_time: start_time + }; + events_to_update[event_id] = params; + delete events_to_create[event_id]; + } + else{ + events_to_create[event_id] = { + room_id: room_id, + start_time: start_time, + event_id: event_id, + schedule_id: schedule_id + }; + delete events_to_update[event_id]; + } + delete events_to_remove[event_id]; + Schedule.invalidateCells(event, event.attr("length")); + event.appendTo(new_parent); + $("#event-" + event_id).find(".schedule-event-delete-button").show(); + }, + saveEvents: function () { + var errors = ''; + var with_errors = false; + var error_callback = function(data) { + try{ + errors += $.parseJSON(data.responseText).errors; + with_errors = true; + }catch(e){ + with_errors = true; } + } + if(!jQuery.isEmptyObject(events_to_create)){ $.ajax({ - url: my_url, - type: 'DELETE', - success: success_callback, + async: false, + url: ("/admin/conferences/" + conference_id + "/bulk_create"), + type: 'POST', + data: { event_schedules: events_to_create }, error: error_callback, dataType : 'json' }); } - else{ - showError("The event couldn't be unscheduled"); + if(!jQuery.isEmptyObject(events_to_update)){ + $.ajax({ + async: false, + url: ("/admin/conferences/" + conference_id + "/bulk_update"), + type: 'POST', + data: { event_schedules: events_to_update }, + error: error_callback, + dataType : 'json' + }); } - }, - add: function (previous_parent, new_parent, event) { - event.appendTo(new_parent); - var event_schedule_id = event.attr("event_schedule_id"); - var my_url = url; - var type = 'POST'; - var params = { event_schedule: { - room_id: new_parent.attr("room_id"), - start_time: (new_parent.attr("date") + ' ' + new_parent.attr("hour")) - }}; - if(event_schedule_id != null){ - type = 'PUT'; - my_url += ('/' + event_schedule_id); + if(!jQuery.isEmptyObject(events_to_remove)){ + $.ajax({ + async: false, + url: ("/admin/conferences/" + conference_id + "/bulk_destroy"), + type: 'POST', + data: { event_schedules: events_to_remove }, + error: error_callback, + dataType : 'json' + }); } - else{ - params['event_schedule']['event_id'] = event.attr("event_id"); - params['event_schedule']['schedule_id'] = schedule_id; - } - var success_callback = function(data) { - console.log(data); - event.attr("event_schedule_id", data.event_schedule_id); - event.find(".schedule-event-delete-button").show(); + var url = (window.location.href).substring(0, window.location.href.indexOf('?')); + if(with_errors){ + var msg = "Some events couldn't be scheduled" + if(errors != ''){ + msg += (": " + errors); } - var error_callback = function(data) { - console.log(data); - showError($.parseJSON(data.responseText).errors); - event.appendTo(previous_parent); + window.location.href = (url + "?flash=" + msg + "&type=error"); + } else { + window.location.href = (url + "?flash=Schedule correctly saved&type=notice"); } - $.ajax({ - url: my_url, - type: type, - data: params, - success: success_callback, - error: error_callback, - dataType : 'json' - }); } }; @@ -86,8 +133,17 @@ $(document).ready( function() { snap: '.schedule-room-slot', revertDuration: 200, revert: function (event, ui) { - console.log(event.attr); - return !event; + var dropable = $(this).parent(); + if(!dropable.hasClass('unscheduled-events')){ + Schedule.invalidateCells(dropable, $(this).attr("length")); + } + return !event; + }, + start: function(event, ui) { + var dropable = $(this).parent(); + if(!dropable.hasClass('unscheduled-events')){ + Schedule.validateCells(dropable, $(this).attr("length")); + } }, stop: function(event, ui) { this._originalPosition = this._originalPosition || ui.originalPosition; @@ -102,11 +158,23 @@ $(document).ready( function() { $('.schedule-room-slot').droppable({ accept: '.schedule-event', tolerance: "pointer", + accept: function(dropElem){ + var free = true; + var i = 0; + var elem = $(this); + while(free && i < dropElem.attr("length")){ + if(elem.hasClass('with-event')) + free = false; + elem = elem.next(); + i++; + } + return free; + }, drop: function(event, ui) { $(ui.draggable).css("left", 0); $(ui.draggable).css("top", 0); $(this).css("background-color", "#ffffff"); - Schedule.add($(ui.draggable).parent(), $(this), $(ui.draggable)); + Schedule.add($(this), $(ui.draggable)); }, over: function(event, ui) { $(this).css("background-color", "#009ED8"); @@ -115,6 +183,10 @@ $(document).ready( function() { $(this).css("background-color", "#ffffff"); } }); + + $('.schedule-save').on('click', function(e) { + Schedule.saveEvents(); + }); }); function eventClicked(e, element){ diff --git a/app/assets/javascripts/osem-switch.js b/app/assets/javascripts/osem-switch.js index 1bdda72c..b98feb50 100644 --- a/app/assets/javascripts/osem-switch.js +++ b/app/assets/javascripts/osem-switch.js @@ -23,7 +23,7 @@ $(function () { } var callback = function(data) { - showError($.parseJSON(data.responseText).errors); + showMessage($.parseJSON(data.responseText).errors, 'error'); } $.ajax({ url: url, diff --git a/app/controllers/admin/event_schedules_controller.rb b/app/controllers/admin/event_schedules_controller.rb index 04713093..6b8ecc14 100644 --- a/app/controllers/admin/event_schedules_controller.rb +++ b/app/controllers/admin/event_schedules_controller.rb @@ -2,34 +2,40 @@ module Admin class EventSchedulesController < Admin::BaseController load_and_authorize_resource :event_schedule - def create - if @event_schedule.save - render json: { event_schedule_id: @event_schedule.id } - else - render json: { errors: "The event couldn't be scheduled. #{@event_schedule.errors.full_messages.join('. ')}" }, status: 422 - end - end - - def update - if @event_schedule.update(event_schedule_params) - render json: { event_schedule_id: @event_schedule.id } - else - render json: { errors: "The event couldn't be scheduled. #{@event_schedule.errors.full_messages.join('. ')}" }, status: 422 - end - end - - def destroy - if @event_schedule.destroy + def bulk_create + result = EventSchedule.create(params[:event_schedules].values).reject { |p| p.errors.empty? } + if result.empty? render json: {} else - render json: { errors: "The event couldn't be unscheduled. #{@event_schedule.errors.full_messages.join('. ')}" }, status: 422 + event_names = result.collect { |event_schedule| event_schedule.event.try(:title) } + render json: { errors: event_names.to_s }, status: 422 end end - private + def bulk_update + keys = [] + values = [] + params[:event_schedules].values.each do |e| + keys << e.keys[0] + values << e.values[0] + end + result = EventSchedule.update(keys, values).reject { |p| p.errors.empty? } + if result.empty? + render json: {} + else + event_names = result.collect { |event_schedule| event_schedule.event.try(:title) } + render json: { errors: event_names.to_s }, status: 422 + end + end - def event_schedule_params - params.require(:event_schedule).permit(:schedule_id, :event_id, :room_id, :start_time) + def bulk_destroy + result = EventSchedule.destroy(params[:event_schedules].values).reject { |p| p.errors.empty? } + if result.empty? + render json: {} + else + event_names = result.collect { |event_schedule| event_schedule.event.try(:title) } + render json: { errors: event_names.to_s }, status: 422 + end end end end diff --git a/app/models/event_schedule.rb b/app/models/event_schedule.rb index cb630acd..fbf5de69 100644 --- a/app/models/event_schedule.rb +++ b/app/models/event_schedule.rb @@ -11,6 +11,8 @@ class EventSchedule < ActiveRecord::Base validates :start_time, presence: true validates :event, uniqueness: { scope: :schedule } + validate :not_overlapping + scope :confirmed, -> { joins(:event).where('state = ?', 'confirmed') } scope :canceled, -> { joins(:event).where('state = ?', 'canceled') } scope :withdrawn, -> { joins(:event).where('state = ?', 'withdrawn') } @@ -36,4 +38,15 @@ class EventSchedule < ActiveRecord::Base def conference_id schedule.program.conference_id end + + def not_overlapping + if room + room.event_schedules.where(schedule: schedule).where.not(id: id).each do |e| + if (e.start_time <= start_time && e.end_time > start_time) || (e.end_time >= end_time && e.start_time < end_time) || (e.start_time > start_time && e.start_time < end_time) + errors.add(:event, "can't be scheduled at the same time than other event in the same room") + break + end + end + end + end end diff --git a/app/views/admin/schedules/_day_tab.html.haml b/app/views/admin/schedules/_day_tab.html.haml index b7cc27e8..179398e6 100644 --- a/app/views/admin/schedules/_day_tab.html.haml +++ b/app/views/admin/schedules/_day_tab.html.haml @@ -5,17 +5,22 @@ .room-name - room_date_event_schedules = date_event_schedules.select{ |e| e.room == room } = room.name + - cells_with_event = 0 - (9*4..18*4).each do |slot| - hour = slot / 4 - minutes = (15 * (slot % 4) == 0) ? '00' : 15 * (slot % 4) - time = "#{hour}:#{minutes}" - .schedule-room-slot{ id: "schedule-room-#{room.guid}-#{hour}-#{minutes}", | - room_id: room.id, | - hour: time, | - date: date} + - event_schedules = room_date_event_schedules.select{ |e| (e.start_time.hour.to_s + e.start_time.strftime(':%M')).eql? time } + - if event_schedules.any? + - event_schedule = event_schedules.first + - cells_with_event = event_schedule.event.event_type.length / EventType::LENGTH_STEP + %div{ class: "schedule-room-slot #{'with-event' if cells_with_event > 0}", | + id: "schedule-room-#{room.guid}-#{hour}-#{minutes}", | + room_id: room.id, | + hour: time, | + date: date} .div = time - - event_schedules = room_date_event_schedules.select{ |e| (e.start_time.hour.to_s + e.start_time.strftime(':%M')).eql? time } - - if event_schedules.any? - - event_schedule = event_schedules.first + - if event_schedule = render partial: 'event', locals: { event: event_schedule.event, event_schedule_id: event_schedule.id} + - (cells_with_event = cells_with_event - 1) if cells_with_event > 0 diff --git a/app/views/admin/schedules/show.html.haml b/app/views/admin/schedules/show.html.haml index 9b2d3571..8a139796 100644 --- a/app/views/admin/schedules/show.html.haml +++ b/app/views/admin/schedules/show.html.haml @@ -4,8 +4,14 @@ .col-md-12 .page-header %h1 Schedule - %p.text-muted - Create the schedules for the conference + .row + .col-md-10.col-xs-8 + %p.text-muted + Create the schedules for the conference + .col-md-2.col-xs-4 + .text-right + %button.btn.btn-success.schedule-save + Save - if @rooms.present? .row @@ -32,6 +38,10 @@ - @dates.each do |date| .tab-pane{ class: "#{ (@dates.first == date) ? 'active' : '' }", id: "#{date}" } = render partial: 'day_tab', locals: { date: date } + .row + .col-md-12.text-right + %button.btn.btn-success.schedule-save + Save - else .h3 No Rooms! @@ -41,5 +51,13 @@ :javascript $(document).ready( function() { - Schedule.initialize("#{admin_conference_event_schedules_path(@conference)}", "#{@schedule.id}"); + // When reload the page after saving, show flash messages in the url. + var url = window.location.href; + if (url.indexOf("?") > 0){ + var params = (url.split("?"))[1].split(/[&=]/); + if(params[0] == 'flash' && params[2] == 'type') + showMessage(decodeURI(params[1]), params[3]); + } + + Schedule.initialize("#{@conference.short_title}", "#{@schedule.id}"); }); diff --git a/config/routes.rb b/config/routes.rb index 2a44f93a..83f22abe 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,7 +28,9 @@ Osem::Application.routes.draw do resources :conferences do resource :contact, except: [:index, :new, :create, :show, :destroy] resources :schedules, only: [:index, :create, :show, :update, :destroy] - resources :event_schedules, only: [:create, :update, :destroy] + post '/bulk_create' => 'event_schedules#bulk_create' + post '/bulk_update' => 'event_schedules#bulk_update' + post '/bulk_destroy' => 'event_schedules#bulk_destroy' get 'commercials/render_commercial' => 'commercials#render_commercial' resources :commercials, only: [:index, :create, :update, :destroy] get '/volunteers_list' => 'volunteers#show' diff --git a/spec/controllers/admin/event_schedules_controller_spec.rb b/spec/controllers/admin/event_schedules_controller_spec.rb index 4d0569ec..88c29a54 100644 --- a/spec/controllers/admin/event_schedules_controller_spec.rb +++ b/spec/controllers/admin/event_schedules_controller_spec.rb @@ -4,30 +4,39 @@ describe Admin::EventSchedulesController do let(:venue) { create(:venue) } let(:conference) { create(:conference, venue: venue) } let(:room) { create(:room, venue: venue) } - let(:schedule) { create(:schedule, program: conference.program)} - let(:event_schedule) { create(:event_schedule, schedule: schedule)} + let(:schedule) { create(:schedule, program: conference.program) } let!(:organizer_role) { Role.find_by(name: 'organizer', resource: conference) } let(:organizer) { create(:user, role_ids: organizer_role.id) } context 'logged in as an organizer' do before :each do sign_in(organizer) - event_schedule end describe 'POST #create' do context 'with valid attributes' do + let(:event1) { create(:event, program: conference.program) } + let(:event2) { create(:event, program: conference.program) } let(:create_action) do - post :create, conference_id: conference.short_title, event_schedule: - attributes_for(:event_schedule, - schedule_id: schedule.id, - event_id: create(:event, program: conference.program).id, - room_id: create(:room, venue: venue).id, - start_time: conference.start_date) + post :bulk_create, conference_id: conference.short_title, + event_schedules: { + event1.id => { + schedule_id: schedule.id, + event_id: event1.id, + room_id: create(:room, venue: venue).id, + start_time: conference.start_date + }, + event2.id => { + schedule_id: schedule.id, + event_id: event2.id, + room_id: create(:room, venue: venue).id, + start_time: conference.start_date + } + } end it 'saves the event schedule to the database' do - expect{ create_action }.to change { EventSchedule.count }.by 1 + expect{ create_action }.to change { EventSchedule.count }.by 2 end it 'has 200 status code' do @@ -37,14 +46,17 @@ describe Admin::EventSchedulesController do end context 'with invalid attributes' do - + let(:event1) { create(:event, program: conference.program) } let(:create_action) do - post :create, conference_id: conference.short_title, event_schedule: - attributes_for(:event_schedule, - schedule_id: schedule.id, - event_id: nil, - room_id: nil, - start_time: nil) + post :bulk_create, conference_id: conference.short_title, + event_schedules: { + event1.id => { + schedule_id: schedule.id, + event_id: event1.id, + room_id: nil, + start_time: nil + } + } end it 'does not save the event schedule to the database' do @@ -60,22 +72,38 @@ describe Admin::EventSchedulesController do describe 'POST #update' do context 'with valid attributes' do + let(:event1) { create(:event, program: conference.program) } + let(:event2) { create(:event, program: conference.program) } + let(:event_schedule1) { create(:event_schedule, schedule: schedule, event: event1) } + let(:event_schedule2) { create(:event_schedule, schedule: schedule, event: event2) } before :each do - patch :update, id: event_schedule.id, conference_id: conference.short_title, event_schedule: - attributes_for(:event_schedule, - schedule_id: schedule.id, - event_id: create(:event, program: conference.program).id, - room_id: room.id, - start_time: conference.start_date) - event_schedule.reload + post :bulk_update, conference_id: conference.short_title, + event_schedules: { + event1.id => { + event_schedule1.id => { + room_id: room.id, + start_time: conference.start_date + } + }, + event2.id => { + event_schedule2.id => { + room_id: room.id, + start_time: conference.start_date + 1.day + } + } + } + event_schedule1.reload + event_schedule2.reload end it 'updates the room' do - expect(event_schedule.room_id).to eq(room.id) + expect(event_schedule1.room_id).to eq(room.id) + expect(event_schedule2.room_id).to eq(room.id) end it 'updates the start_time' do - expect(event_schedule.start_time).to eq(conference.start_date) + expect(event_schedule1.start_time).to eq(conference.start_date) + expect(event_schedule2.start_time).to eq(conference.start_date + 1.day) end it 'has 200 status code' do @@ -84,13 +112,18 @@ describe Admin::EventSchedulesController do end context 'with invalid attributes' do + let(:event) { create(:event, program: conference.program) } + let(:event_schedule) { create(:event_schedule, schedule: schedule, event: event) } let(:update_action) do - patch :update, id: event_schedule.id, conference_id: conference.short_title, event_schedule: - attributes_for(:event_schedule, - schedule_id: schedule.id, - event_id: nil, - room_id: nil, - start_time: nil) + post :bulk_update, conference_id: conference.short_title, + event_schedules: { + event.id => { + event_schedule.id => { + room_id: nil, + start_time: nil + } + } + } end it 'does not save the event schedule to the database' do expect{ update_action }.to_not change { event_schedule } @@ -104,12 +137,22 @@ describe Admin::EventSchedulesController do end describe 'DELETE #destroy' do + let(:event1) { create(:event, program: conference.program) } + let(:event2) { create(:event, program: conference.program) } + let(:event_schedule1) { create(:event_schedule, schedule: schedule, event: event1) } + let(:event_schedule2) { create(:event_schedule, schedule: schedule, event: event2) } let(:destroy_action) do - delete :destroy, id: event_schedule.id, conference_id: conference.short_title + post :bulk_destroy, conference_id: conference.short_title, + event_schedules: { + event1.id => event_schedule1, + event2.id => event_schedule2 + } end it 'deletes the event schedule' do - expect{ destroy_action }.to change { EventSchedule.count }.by(-1) + event_schedule1.reload + event_schedule2.reload + expect{ destroy_action }.to change { EventSchedule.count }.by(-2) end it 'has 200 status code' do diff --git a/spec/models/event_schedule_spec.rb b/spec/models/event_schedule_spec.rb index 28b04ddf..33ce4edb 100644 --- a/spec/models/event_schedule_spec.rb +++ b/spec/models/event_schedule_spec.rb @@ -17,5 +17,32 @@ describe EventSchedule do it { is_expected.to validate_presence_of(:event) } it { is_expected.to validate_presence_of(:room) } it { is_expected.to validate_presence_of(:start_time) } + + describe '#not_overlapping' do + let!(:event) { create(:event, event_type: create(:event_type, length: 60)) } + let!(:event2) { create(:event, event_type: create(:event_type, length: 30), program: event.program) } + let!(:schedule) { create(:schedule, program: event.program) } + let!(:room) { create(:room, venue: create(:venue, conference: event.program.conference)) } + let!(:event_schedule) { create(:event_schedule, schedule: schedule, event: event, room: room, start_time: event.program.conference.start_date.tomorrow.to_time + 60.minutes) } + + describe "can't be scheduled at the same time than other event in the same room" do + it 'case 1' do + expect(build(:event_schedule, schedule: schedule, event: event2, room: room, start_time: event_schedule.start_time - 15.minutes)).to_not be_valid + end + + it 'case 2' do + expect(build(:event_schedule, schedule: schedule, event: event2, room: room, start_time: event_schedule.start_time + 45.minutes)).to_not be_valid + end + + it 'case 3' do + expect(build(:event_schedule, schedule: schedule, event: event2, room: room, start_time: event_schedule.start_time + 15.minutes)).to_not be_valid + end + + it 'case 4' do + event2.event_type = create(:event_type, length: 120) + expect(build(:event_schedule, schedule: schedule, event: event2, room: room, start_time: event_schedule.start_time - 30.minutes)).to_not be_valid + end + end + end end end