diff --git a/.github/workflows/spec.yml b/.github/workflows/spec.yml index b2e7d847..6875c7b8 100644 --- a/.github/workflows/spec.yml +++ b/.github/workflows/spec.yml @@ -38,6 +38,12 @@ jobs: with: ruby-version: 2.6.6 bundler-cache: true + - name: Use Node.js + uses: actions/setup-node@v1 + with: + node-version: '12.x' + - name: Install JavaScript libraries via npm + run: npm install - name: Prepare spec run: | rm -f osem_test osem_development diff --git a/.gitignore b/.gitignore index c65dc0b1..fcdf9332 100644 --- a/.gitignore +++ b/.gitignore @@ -121,3 +121,6 @@ build-iPhoneSimulator/ # Used by RuboCop. Remote config files pulled in from inherit_from directive. .rubocop-https?--* + +# Ignore Javascript package binaries +/node_modules diff --git a/.travis.yml b/.travis.yml index b35784c6..00d5b582 100644 --- a/.travis.yml +++ b/.travis.yml @@ -43,6 +43,7 @@ jobs: - gem install bundler -v $(tail -n 1 Gemfile.lock) - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > $CCTR - chmod +x $CCTR + - npm install before_script: - RAILS_ENV=test bundle exec rake db:bootstrap --trace - RAILS_ENV=test bundle exec bin/rails webdrivers:chromedriver:update diff --git a/Gemfile b/Gemfile index 8627c685..7c1eb7f1 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,7 @@ gem 'paper_trail' # for upload management gem 'carrierwave' gem 'carrierwave-bombshelter' -gem 'mimemagic', '~> 0.3.6' +gem 'mimemagic' gem 'mini_magick' # for internationalizing diff --git a/Gemfile.lock b/Gemfile.lock index 24bc65a3..10c6a0bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -723,7 +723,7 @@ DEPENDENCIES leaflet-rails letter_opener letter_opener_web (~> 1.0) - mimemagic (~> 0.3.6) + mimemagic mina mini_magick money-rails diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js index 6548a3fe..c5b49ae7 100644 --- a/app/assets/javascripts/application.js +++ b/app/assets/javascripts/application.js @@ -55,6 +55,8 @@ //= require bootstrap-select //= require osem-survey //= require pagy +//= require fullcalendar-scheduler/main.js +//= require fullcalendar $(document).ready(function() { $('a[disabled=disabled]').click(function(event){ diff --git a/app/assets/javascripts/fullcalendar.js.erb b/app/assets/javascripts/fullcalendar.js.erb new file mode 100644 index 00000000..992bcbc6 --- /dev/null +++ b/app/assets/javascripts/fullcalendar.js.erb @@ -0,0 +1,30 @@ +$( document ).ready(function() { + let calendarEl = document.getElementById('vert-schedule-full-calendar'); + if (!calendarEl) return; //check that we need a vertical schedule + let fullcalData = $('#fullcalendar'); + + let license_key = "<%= Rails.configuration.fullcalendar[:license_key] %>"; + + var calendar = new FullCalendar.Calendar(calendarEl, { + schedulerLicenseKey: license_key, + nowIndicator: true, + now: fullcalData.data('now'), + expandRows: true, + allDaySlot: false, + slotMinTime: fullcalData.data('startHour') + ':00:00', + slotMaxTime: fullcalData.data('endHour') + ':00:00', + validRange: { + start: fullcalData.data('startDate'), + end: fullcalData.data('endDate') + }, + timeZone: fullcalData.data('timezone'), + initialDate: fullcalData.data('day'), + initialView: 'resourceTimeGridDay', + resources: fullcalData.data('rooms'), + events: fullcalData.data('events') + }); + + calendar.render(); +}); + + diff --git a/app/assets/stylesheets/application.css b/app/assets/stylesheets/application.css index f5c58982..520eedbe 100644 --- a/app/assets/stylesheets/application.css +++ b/app/assets/stylesheets/application.css @@ -26,4 +26,6 @@ *= require osem-navbar *= require mastodon *= require conferences + + *= require fullcalendar-scheduler/main.css */ diff --git a/app/controllers/schedules_controller.rb b/app/controllers/schedules_controller.rb index bdefb939..e0c5f911 100644 --- a/app/controllers/schedules_controller.rb +++ b/app/controllers/schedules_controller.rb @@ -78,6 +78,25 @@ class SchedulesController < ApplicationController end end + def vertical_schedule + dates = @conference.start_date..@conference.end_date + # the schedule takes you to today if it is a date of the schedule + current_day = @conference.current_conference_day + @day = current_day.present? ? current_day : dates.first + event_schedules = @program.selected_event_schedules( + includes: [{ event: %i[event_type speakers submitter] }] + ) + + unless event_schedules + redirect_to events_conference_schedule_path(@conference.short_title) + return + end + + @rooms = FullCalendarFormatter.rooms_to_resources(@conference.venue.rooms) if @conference.venue + @event_schedules = FullCalendarFormatter.event_schedules_to_resources(event_schedules) + @now = Time.now.in_time_zone(@conference.timezone).strftime('%FT%T%:z') + end + def app @qr_code = RQRCode::QRCode.new(conference_schedule_url).as_svg(offset: 20, color: '000', shape_rendering: 'crispEdges', module_size: 11) end diff --git a/app/models/ability.rb b/app/models/ability.rb index ec17e14c..68c83066 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -31,7 +31,7 @@ class Ability event.state == 'confirmed' end - can [:show, :events, :happening_now, :app], Schedule do |schedule| + can [:show, :events, :happening_now, :app, :vertical_schedule], Schedule do |schedule| schedule.program.schedule_public end diff --git a/app/models/event_schedule.rb b/app/models/event_schedule.rb index f8a41d64..7be7a7d2 100644 --- a/app/models/event_schedule.rb +++ b/app/models/event_schedule.rb @@ -133,6 +133,14 @@ class EventSchedule < ApplicationRecord other.schedule_id == schedule_id end + def start_time_in_conference_timezone + time_in_conference_timezone(start_time) + end + + def end_time_in_conference_timezone + time_in_conference_timezone(end_time) + end + private def replaced_event_schedules @@ -187,4 +195,10 @@ class EventSchedule < ApplicationRecord errors.add(:schedule, "must be one of #{event.track.name} track's schedules") unless event.track.schedules.include?(schedule) end + + def time_in_conference_timezone(time) + time_in_tz = time.in_time_zone(timezone) + time_in_tz -= time_in_tz.utc_offset + time_in_tz + end end diff --git a/app/services/full_calendar_formatter.rb b/app/services/full_calendar_formatter.rb new file mode 100644 index 00000000..499b8e04 --- /dev/null +++ b/app/services/full_calendar_formatter.rb @@ -0,0 +1,42 @@ +class FullCalendarFormatter + def self.rooms_to_resources(rooms) + rooms.map { |room| room_to_resource(room) }.to_json + end + + def self.event_schedules_to_resources(event_schedules) + return '[]' if event_schedules.empty? + + conference = event_schedules.first.schedule.program.conference + event_schedules.map { |event_schedule| event_schedule_to_resource(conference, event_schedule) }.to_json + end + + class << self + include FormatHelper + + private + + def room_to_resource(room) + { + id: room.guid, + title: room.name + } + end + + def event_schedule_to_resource(conference, event_schedule) + event_type_color = event_schedule.event.event_type.color + url = Rails.application.routes.url_helpers.conference_program_proposal_path(conference.short_title, event_schedule.event.id) + + { + id: event_schedule.event.guid, + title: event_schedule.event.title, + start: event_schedule.start_time_in_conference_timezone, + end: event_schedule.end_time_in_conference_timezone, + resourceId: event_schedule.room.guid, + url: url, + borderColor: event_type_color, + backgroundColor: event_type_color, + textColor: contrast_color(event_type_color) + } + end + end +end diff --git a/app/views/schedules/vertical_schedule.haml b/app/views/schedules/vertical_schedule.haml new file mode 100644 index 00000000..9e5ce379 --- /dev/null +++ b/app/views/schedules/vertical_schedule.haml @@ -0,0 +1,12 @@ +.container#program + = render 'schedule_tabs', active: 'vertical_schedule' + + %h1.text-center + Vertical Schedule for + = @conference.title + + #vert-schedule-full-calendar + #fullcalendar{ data: { rooms: @rooms, events: @event_schedules, day: @day, + 'start-hour': @conference.start_hour, 'end-hour': @conference.end_hour, + 'start-date': @conference.start_date, 'end-date': @conference.end_date + 1.day, + 'timezone': @conference.timezone, 'now': @now } } diff --git a/config/application.rb b/config/application.rb index 4e5e9b87..320e4fd3 100644 --- a/config/application.rb +++ b/config/application.rb @@ -80,5 +80,11 @@ module Osem end end end + + config.assets.paths << Rails.root.join('node_modules') + + config.fullcalendar = { + license_key: ENV['FULL_CALENDAR_LICENSE_KEY'] + } end end diff --git a/config/routes.rb b/config/routes.rb index e2332ce2..740542c8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -201,6 +201,7 @@ Osem::Application.routes.draw do member do get :events get :happening_now + get :vertical_schedule end end end diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..88e0e172 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,11 @@ +{ + "requires": true, + "lockfileVersion": 1, + "dependencies": { + "fullcalendar-scheduler": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/fullcalendar-scheduler/-/fullcalendar-scheduler-5.6.0.tgz", + "integrity": "sha512-0jx/Q+6QqzvIGWU/52OE27UVjVAiM9E0o/qRwRcEhyuKlL54ypTF/yXP/wo0ctUtui5dDsilGNsSsba1xmARJA==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..d8f5ba1a --- /dev/null +++ b/package.json @@ -0,0 +1,5 @@ +{ + "dependencies": { + "fullcalendar-scheduler": "^5.6.0" + } +} diff --git a/spec/controllers/schedules_controller_spec.rb b/spec/controllers/schedules_controller_spec.rb index 22cb7c1a..dbf9e38b 100644 --- a/spec/controllers/schedules_controller_spec.rb +++ b/spec/controllers/schedules_controller_spec.rb @@ -72,4 +72,20 @@ describe SchedulesController do end end end + + describe 'GET #vertical_schedule' do + let!(:program) { conference.program } + + context 'as a conference participant' do + context 'who visits the schedule page' do + before(:each) do + get :vertical_schedule, params: { conference_id: conference.short_title } + end + + it 'returns a successful response' do + expect(response.status).to eq(200) + end + end + end + end end diff --git a/spec/services/full_calendar_formatter_spec.rb b/spec/services/full_calendar_formatter_spec.rb new file mode 100644 index 00000000..63af2d08 --- /dev/null +++ b/spec/services/full_calendar_formatter_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe FullCalendarFormatter do + let!(:room1) { create(:room) } + let!(:room2) { create(:room) } + let!(:rooms) { [room1, room2] } + let!(:conference) { create(:full_conference) } + let!(:program) { conference.program } + let!(:selected_schedule) { create(:schedule, program: program) } + let!(:event_type1) { create(:event_type, color: '#ffffff') } + let!(:event1) do + program.update_attributes!(selected_schedule: selected_schedule) + create(:event, program: program, event_type: event_type1) + end + let!(:event_schedule1) { create(:event_schedule, event: event1, schedule: selected_schedule, room: room1) } + let!(:event_type2) { create(:event_type, color: '#000000') } + let!(:event2) do + program.update_attributes!(selected_schedule: selected_schedule) + create(:event, program: program, room: room2, event_type: event_type2) + end + let!(:event_schedule2) { create(:event_schedule, event: event2, schedule: selected_schedule, room: room2) } + let!(:event_schedules) { [event_schedule1, event_schedule2] } + + describe 'JSON formatting for FullCalendar' do + it 'translates rooms to resources' do + resources = described_class.rooms_to_resources(rooms) + expected_json = [ + { + id: room1.guid, + title: room1.name + }, + { + id: room2.guid, + title: room2.name + } + ].to_json + expect(resources).to eq(expected_json) + end + + it 'translates event schedules to resources' do + resources = described_class.event_schedules_to_resources(event_schedules) + expected_json = [ + { + id: event_schedule1.event.guid, + title: event_schedule1.event.title, + start: event_schedule1.start_time_in_conference_timezone, + end: event_schedule1.end_time_in_conference_timezone, + resourceId: room1.guid, + url: Rails.application.routes.url_helpers.conference_program_proposal_path(conference.short_title, event1.id), + borderColor: event1.event_type.color, + backgroundColor: event1.event_type.color, + textColor: 'black' + }, + { + id: event_schedule2.event.guid, + title: event_schedule2.event.title, + start: event_schedule2.start_time_in_conference_timezone, + end: event_schedule2.end_time_in_conference_timezone, + resourceId: room2.guid, + url: Rails.application.routes.url_helpers.conference_program_proposal_path(conference.short_title, event2.id), + borderColor: event2.event_type.color, + backgroundColor: event2.event_type.color, + textColor: 'white' + } + ].to_json + + expect(resources).to eq(expected_json) + end + end +end