diff --git a/Gemfile b/Gemfile index e96be9d3c..53330492a 100644 --- a/Gemfile +++ b/Gemfile @@ -9,6 +9,7 @@ gem 'rails', '~> 6.1' #, '>= 6.0.3.2' gem 'pg', '>= 0.18', '< 2.0' gem 'activerecord-postgres_enum' +gem 'actionpack-action_caching' # Use Puma as the app server gem 'puma', '>= 5.0' @@ -63,6 +64,10 @@ gem 'truemail' # This is needed to run the migrations from id to uuid for primary keys gem 'webdack-uuid_migration' +# for integrations! +gem "httparty" +gem "json-diff" + group :development, :test do # Call 'byebug' anywhere in the code to stop execution and get a debugger console gem 'byebug', platforms: [:mri, :mingw, :x64_mingw] diff --git a/Gemfile.lock b/Gemfile.lock index 6e8761bad..12f0cbf71 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - aasm (5.2.0) + aasm (5.3.0) concurrent-ruby (~> 1.0) actioncable (6.1.6.1) actionpack (= 6.1.6.1) @@ -29,6 +29,8 @@ GEM rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) + actionpack-action_caching (1.2.2) + actionpack (>= 4.0.0) actiontext (6.1.6.1) actionpack (= 6.1.6.1) activerecord (= 6.1.6.1) @@ -77,9 +79,9 @@ GEM ast (2.4.2) bcrypt (3.1.18) bindex (0.8.1) - bootsnap (1.12.0) + bootsnap (1.13.0) msgpack (~> 1.2) - brakeman (5.2.3) + brakeman (5.3.1) builder (3.2.4) bundler-audit (0.9.1) bundler (>= 1.2.0, < 3) @@ -115,7 +117,7 @@ GEM em-websocket (0.5.3) eventmachine (>= 0.12.9) http_parser.rb (~> 0) - erubi (1.10.0) + erubi (1.11.0) et-orbi (1.2.7) tzinfo eventmachine (1.2.7) @@ -124,7 +126,7 @@ GEM factory_bot_rails (6.2.0) factory_bot (~> 6.2.0) railties (>= 5.0.0) - faker (2.21.0) + faker (2.22.0) i18n (>= 1.8.11, < 2) fast_excel (0.4.0) ffi (> 1.9, < 2) @@ -151,6 +153,9 @@ GEM guard-compat (~> 1.0) multi_json (~> 1.8) http_parser.rb (0.8.0) + httparty (0.20.0) + mime-types (~> 3.0) + multi_xml (>= 0.5.2) i18n (1.12.0) concurrent-ruby (~> 1.0) io-wait (0.2.3) @@ -158,6 +163,7 @@ GEM actionview (>= 5.0.0) activesupport (>= 5.0.0) json (2.6.2) + json-diff (0.4.1) jsonapi-renderer (0.2.2) jsonapi-serializer (2.2.0) activesupport (>= 4.2) @@ -188,34 +194,38 @@ GEM marcel (1.0.2) matrix (0.4.2) method_source (1.0.0) + mime-types (3.4.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2022.0105) mini_mime (1.1.2) mini_portile2 (2.8.0) - minitest (5.16.2) - msgpack (1.5.3) + minitest (5.16.3) + msgpack (1.5.4) multi_json (1.15.0) + multi_xml (0.6.0) nenv (0.3.0) nilify_blanks (1.4.0) activerecord (>= 4.0.0) activesupport (>= 4.0.0) nio4r (2.5.8) - nokogiri (1.13.7) + nokogiri (1.13.8) mini_portile2 (~> 2.8.0) racc (~> 1.4) - nokogiri (1.13.7-x86_64-darwin) + nokogiri (1.13.8-x86_64-darwin) racc (~> 1.4) - nokogiri (1.13.7-x86_64-linux) + nokogiri (1.13.8-x86_64-linux) racc (~> 1.4) notiffany (0.1.3) nenv (~> 0.1) shellany (~> 0.0) orm_adapter (0.5.0) - paper_trail (12.3.0) + paper_trail (13.0.0) activerecord (>= 5.2) request_store (~> 1.1) parallel (1.22.1) - parser (3.1.2.0) + parser (3.1.2.1) ast (~> 2.4.1) - pg (1.4.1) + pg (1.4.3) pry (0.14.1) coderay (~> 1.1) method_source (~> 1.0) @@ -304,17 +314,17 @@ GEM rspec-mocks (~> 3.10) rspec-support (~> 3.10) rspec-support (3.11.0) - rubocop (1.31.2) + rubocop (1.35.0) json (~> 2.3) parallel (~> 1.10) - parser (>= 3.1.0.0) + parser (>= 3.1.2.1) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 1.8, < 3.0) rexml (>= 3.2.5, < 4.0) - rubocop-ast (>= 1.18.0, < 2.0) + rubocop-ast (>= 1.20.1, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 1.4.0, < 3.0) - rubocop-ast (1.19.1) + rubocop-ast (1.21.0) parser (>= 3.1.1.0) ruby-progressbar (1.11.0) ruby_audit (2.1.0) @@ -334,17 +344,17 @@ GEM tilt seedbank (0.5.0) rake (>= 10.0) - selenium-webdriver (4.3.0) + selenium-webdriver (4.4.0) childprocess (>= 0.5, < 5.0) rexml (~> 3.2, >= 3.2.5) rubyzip (>= 1.2.2, < 3.0) websocket (~> 1.0) semantic_range (3.0.0) shellany (0.0.1) - sidekiq (6.5.1) + sidekiq (6.5.4) connection_pool (>= 2.2.2) rack (~> 2.0) - redis (>= 4.2.0) + redis (>= 4.5.0) sidekiq-scheduler (4.0.2) redis (>= 4.2.0) rufus-scheduler (~> 3.2) @@ -364,10 +374,10 @@ GEM activesupport (>= 5.2) sprockets (>= 3.0.0) thor (1.2.1) - tilt (2.0.10) - truemail (2.7.3) + tilt (2.0.11) + truemail (2.7.4) simpleidn (~> 0.2.1) - tzinfo (2.0.4) + tzinfo (2.0.5) concurrent-ruby (~> 1.0) unf (0.1.4) unf_ext @@ -407,6 +417,7 @@ PLATFORMS DEPENDENCIES aasm + actionpack-action_caching active_model_serializers (~> 0.10.0) activerecord-postgres_enum acts-as-taggable-on (~> 9.0) @@ -423,8 +434,10 @@ DEPENDENCIES fast_excel guard guard-livereload + httparty io-wait jbuilder (~> 2.7) + json-diff jsonapi-serializer jsonapi.rb kaminari diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8ae0032ea..bbba2594d 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -92,8 +92,8 @@ def configure_permitted_parameters def render_jsonapi_internal_server_error(exception) # Rails.logger.error "**** INTERNAL ERROR #{exception.class}" - Rails.logger.error exception.message if Rails.env.development? - Rails.logger.error exception.backtrace.join("\n\t") if Rails.env.development? + Rails.logger.error exception.message #if Rails.env.development? + Rails.logger.error exception.backtrace.join("\n\t") #if Rails.env.development? # NOTE: if we have a central log put it in here super(exception) end diff --git a/app/controllers/integrations_controller.rb b/app/controllers/integrations_controller.rb new file mode 100644 index 000000000..8bcd1bc0c --- /dev/null +++ b/app/controllers/integrations_controller.rb @@ -0,0 +1,28 @@ +class IntegrationsController < ResourceController + SERIALIZER_CLASS = 'IntegrationSerializer'.freeze + POLICY_CLASS = 'IntegrationPolicy'.freeze + + def airmeet + authorize model_class, policy_class: policy_class + + airmeet = Integration.find_by({name: 'airmeet'}) + + render json: serializer_class.new(airmeet, + { + include: serializer_includes, + params: {domain: "#{request.base_url}"} + } + ).serializable_hash(), + content_type: 'application/json' + end + + def allowed_params + %i[ + id + name + lock_version + config + ] + end + +end diff --git a/app/controllers/publication_dates_controller.rb b/app/controllers/publication_dates_controller.rb index 971d4b442..3048de63e 100644 --- a/app/controllers/publication_dates_controller.rb +++ b/app/controllers/publication_dates_controller.rb @@ -7,18 +7,20 @@ class PublicationDatesController < ResourceController def reset # Make sure that people can not publish etc while we clean up - PublicationStatus.last.update(status: 'inprogress') + if PublicationStatus.last + PublicationStatus.last.update(status: 'inprogress') - # Get rid of published data first - PublishedSession.destroy_all + # Get rid of published data first + PublishedSession.destroy_all - # Then get rid of the audit - Audit::PublishedSessionVersion.delete_all + # Then get rid of the audit + Audit::PublishedSessionVersion.delete_all - # Then remove caches, dates, and status - PublishSnapshot.delete_all - PublicationDate.delete_all - PublicationStatus.delete_all + # Then remove caches, dates, and status + PublishSnapshot.delete_all + PublicationDate.delete_all + PublicationStatus.delete_all + end render status: :ok, json: { message: 'publication reset' }.to_json, diff --git a/app/controllers/published_sessions_controller.rb b/app/controllers/published_sessions_controller.rb index e0bf0d590..615a3c781 100644 --- a/app/controllers/published_sessions_controller.rb +++ b/app/controllers/published_sessions_controller.rb @@ -1,3 +1,5 @@ class PublishedSessionsController < ResourceController -# TBD + SERIALIZER_CLASS = 'PublishedSessionSerializer'.freeze + POLICY_CLASS = 'PublishedSessionPolicy'.freeze +# end diff --git a/app/controllers/reports/program_ops_reports_controller.rb b/app/controllers/reports/program_ops_reports_controller.rb index 628123236..cc910ce5f 100644 --- a/app/controllers/reports/program_ops_reports_controller.rb +++ b/app/controllers/reports/program_ops_reports_controller.rb @@ -1,6 +1,52 @@ class Reports::ProgramOpsReportsController < ApplicationController around_action :set_timezone + def virtual_people + authorize SessionAssignment, policy_class: Reports::ProgramOpsReportPolicy + + moderator = SessionAssignmentRoleType.find_by(name: 'Moderator') + participant = SessionAssignmentRoleType.find_by(name: 'Participant') + + assignments = PublishedSessionAssignment + .includes(:person, :session_assignment_role_type, :published_session) + .where("session_assignment_role_type_id in (?)", [moderator.id, participant.id]) + .order("people.published_name") + + workbook = FastExcel.open(constant_memory: true) + worksheet = workbook.add_worksheet("Virtual Participants") + + worksheet.append_row( + [ + 'Published Name', + 'Name', + 'Is Virtual', + 'Primary Email' + ] + ) + + group_assignments = assignments.group_by {|a| a.person} + group_assignments.each do |person, grouped| + row = [ + person.published_name, + person.name, + grouped.reduce(false){|res, a| res || (a.published_session.environment == 'virtual')} ? 'Yes' : 'No', # grouped contains virtual + person.primary_email.email + ] + + person.email_addresses.each do |addr| + next if addr == person.primary_email + + row.concat [addr.email] + end + + worksheet.append_row(row) + end + + send_data workbook.read_string, + filename: "VirtualParticipants-#{Time.now.strftime('%m-%d-%Y')}.xlsx", + disposition: 'attachment' + end + def back_of_badge authorize SessionAssignment, policy_class: Reports::ProgramOpsReportPolicy @@ -27,7 +73,7 @@ def back_of_badge row.concat [ assignment.session.title, assignment.session.start_time ? FastExcel.date_num(assignment.session.start_time, assignment.session.start_time.in_time_zone.utc_offset) : nil, - "#{assignment.session.duration} mins", + "#{assignment.session.duration}m", assignment.session.room&.name, ] styles.concat [ diff --git a/app/controllers/reports/schedule_reports_controller.rb b/app/controllers/reports/schedule_reports_controller.rb index a3811d346..f3cc67451 100644 --- a/app/controllers/reports/schedule_reports_controller.rb +++ b/app/controllers/reports/schedule_reports_controller.rb @@ -1,189 +1,262 @@ class Reports::ScheduleReportsController < ApplicationController around_action :set_timezone + # + # Get the sched diffs based on "publication dates" + # def schedule_diff authorize Person, policy_class: Reports::ScheduleReportPolicy - # TODO: base the from and to on the publish ids and this will be ok as a get pd_from = PublicationDate.find params[:from] if params[:from] from = pd_from.timestamp if pd_from from ||= PublicationDate.order('created_at desc').first&.timestamp - from ||= Time.now - 2.year # TODO: for testing + from ||= Time.now - 2.year pd_to = PublicationDate.find params[:to] if params[:to] to = pd_to.timestamp if pd_to live = false - # changes = ChangeService.published_changes(from: from, to: to) changes = if to ChangeService.published_changes(from: from, to: to) else live = true ChangeService.session_changes(from: from) end + fully_dropped = ChangeService.dropped_people(from: from, to: to) to ||= Time.now - workbook = FastExcel.open(constant_memory: true) - date_time_style = workbook.number_format(EXCEL_NBR_FORMAT) - session_time_changed = workbook.add_worksheet("Session Time Changed") - session_room_changed = workbook.add_worksheet("Session Room Changed") - session_title_changed = workbook.add_worksheet("Session Title Changed") - session_description_changed = workbook.add_worksheet("Session Description Changed") - session_added = workbook.add_worksheet("Sessions Add") - session_removed = workbook.add_worksheet("Sessions Removed") + workbook = FastExcel.open(constant_memory: true) + init_sheets(workbook: workbook) - participants_fully_dropped = workbook.add_worksheet("Participants Dropped") - participants_add_drop = workbook.add_worksheet("Participants Add Drop") + # Rails.logger.debug "******** CHANGES: #{changes[:sessions]}" + state_change_sessions = check_sessions_changed( + changes: changes[:sessions], + to: to, + live: live + ) + check_assignments_changed( + changes: changes[:assignments], + state_change_sessions: state_change_sessions, + to: to + ) - tab_headers(session_time_changed, session_room_changed, session_title_changed, session_description_changed, session_added, session_removed, participants_add_drop, participants_fully_dropped) + fully_dropped.uniq.each do |name| + @participants_fully_dropped.append_row(name) + end - # Rails.logger.debug "******** CHANGES: #{changes[:sessions]}" + send_data workbook.read_string, + filename: "ScheduleDiff-#{from.strftime('%m-%d-%Y_%H_%M')}_to_#{to.strftime('%m-%d-%Y_%H_%M')}.xlsx", + disposition: 'attachment' + end + # + # + # + def check_sessions_changed(changes:, to:, live:) state_change_sessions = [] - changes[:sessions].values.sort{|a,b| (a[:object] ? a[:object].title : '') <=> (b[:object] ? b[:object].title : '')}.each do |change| - # Rails.logger.debug "******** CHANGE: #{change}" + changes.values.sort{|a,b| (a[:object] ? a[:object].title : '') <=> (b[:object] ? b[:object].title : '')}.each do |change| next unless change[:object] - if change[:changes]['room_id'] || change[:changes]['start_time'] - if change[:changes]['room_id'] && change[:changes]['start_time'] && - ((!change[:changes]['room_id'][0] && change[:changes]['room_id'][1]) || - (!change[:changes]['start_time'][0] && change[:changes]['start_time'][1])) - session_added_row(session_added, change, date_time_style) - live_add(session: change[:object], sheet: participants_add_drop) if live + if change[:object] && session_status_change_to_publishable?(change: change) || session_status_change_to_drop?(change: change) + state_change_sessions << change[:object].id + end + + if (change[:changes]['room_id'] || change[:changes]['start_time']) #&& !change[:changes]['status'] + next if ignore_session_status_change?(change: change) + next if live && !change[:changes]['status'] && ['draft', 'dropped'].include?(change[:object].status) + + # Rails.logger.debug "******** SESSION ADD/REMOVE #{change[:changes]} " + # If either room or time was added to the session + if room_added?(change) || start_time_added?(change) + session_added_row(@session_added, change, to) + live_add(change: change, sheet: @participants_add_drop) if live next - else - if (change[:changes]['room_id'] && !change[:changes]['room_id'][1]) || (change[:changes]['start_time'] && !change[:changes]['start_time'][1])|| change[:event] == 'destroy' - # Rails.logger.debug "******** removed because room or time" - session_removed_row(session_removed, change) - live_drop(session: change[:object], sheet: participants_add_drop) if live - next - else - if change[:changes]['room_id'] - session_room_change_row(session_room_changed, change) - end - if change[:changes]['start_time'] - session_time_change_row(session_time_changed, change, date_time_style) - end - end + end + + # If either room or time was removed to the session + if room_removed?(change) || start_time_removed?(change) + session_removed_row(@session_removed, change) + live_drop(session: change[:object], sheet: @participants_add_drop) if live + next + end + + # If the room was changed + if change[:changes]['room_id'] + session_room_change_row(@session_room_changed, change) + end + + # If the time was changed + if change[:changes]['start_time'] + session_time_change_row(@session_time_changed, change) end end + # If the object is scheduked and title or description was changed if change[:object].start_time && change[:object].room_id - if change[:changes]['status'] - if (change[:changes]['status'][1] == 'draft' || change[:changes]['status'][1] == 'dropped') - # Rails.logger.debug "******** removed because draft" - session_removed_row(session_removed, change) - if live - live_drop(session: change[:object], sheet: participants_add_drop) - state_change_sessions << change[:object].id - end - next - elsif (change[:changes]['status'][0] == 'draft' || change[:changes]['status'][0] == 'dropped') - # Rails.logger.debug "******** added because state change" - session_added_row(session_added, change, date_time_style) - if live - live_add(session: change[:object], sheet: participants_add_drop) - state_change_sessions << change[:object].id - end - next - end - end + check_status_change(change: change, to: to, live: live) if change[:changes]['title'] - session_title_change_row(session_title_changed, change) + session_title_change_row(@session_title_changed, change) end if change[:changes]['description'] - session_description_change_row(session_description_changed, change) + session_description_change_row(@session_description_changed, change) end end end - # Deal with the assignment changes + # Rails.logger.debug "*********** ST #{state_change_sessions}" + return state_change_sessions + end + + def room_added?(change) + return false unless change[:changes] && change[:changes]['room_id'] + + !change[:changes]['room_id'][0] && change[:changes]['room_id'][1] + end + + def start_time_added?(change) + return false unless change[:changes] && change[:changes]['start_time'] + + !change[:changes]['start_time'][0] && change[:changes]['start_time'][1] + end + + def room_removed?(change) + return false unless change[:changes] && change[:changes]['room_id'] + + change[:changes]['room_id'][0] && !change[:changes]['room_id'][1] + end + + def start_time_removed?(change) + return false unless change[:changes] && change[:changes]['start_time'] + + change[:changes]['start_time'][0] && !change[:changes]['start_time'][1] + end + + def check_assignments_changed(changes:, state_change_sessions:, to:) moderator = SessionAssignmentRoleType.find_by(name: 'Moderator') participant = SessionAssignmentRoleType.find_by(name: 'Participant') roles = [moderator.id, participant.id] - # Rails.logger.debug "******** ROLES #{roles}" - # TODO: sort .... ??? how - fully_dropped = [] - changes[:assignments].each do |id, change| - # next unless change[:object] + changes.each do |id, change| changed_assignment = change[:object] - changed_assignment ||= SessionAssignment.find change[:session_assignment_id] - next if state_change_sessions.include? change[:object].session_id + changed_assignment ||= SessionAssignment.find(id) if SessionAssignment.exists?(id) + + next unless changed_assignment + next if state_change_sessions.include?(changed_assignment.session_id || changed_assignment.published_session_id) - session = changed_assignment.session if changed_assignment.session - session ||= Session.find changed_assignment.session_id + session = ChangeService.session_as_of(session_id: changed_assignment.session_id, to: to) + next unless session - # Rails.logger.debug "******** Assignment changes #{change}" + person ||= changed_assignment.person + next unless person # Participants add/drop if change[:changes]['session_assignment_role_type_id'] && change[:event] != 'destroy' if session.start_time && session.room_id if (roles.include?(change[:changes]['session_assignment_role_type_id'][1])) - participants_add_drop.append_row( + @participants_add_drop.append_row( [ session.title, '', - changed_assignment.person.published_name + person.published_name ] ) elsif (roles.include?(change[:changes]['session_assignment_role_type_id'][0])) - participants_add_drop.append_row( + @participants_add_drop.append_row( [ session.title, - changed_assignment.person.published_name, + person.published_name, ] ) - if changed_assignment.person.sessions.scheduled.count == 0 - fully_dropped.append [change[:object].person.published_name] - end end end else # Rails.logger.debug "******** Assignment EVENT DROP ????" if change[:event] == 'destroy' - participants_add_drop.append_row( + @participants_add_drop.append_row( [ session.title, - changed_assignment.person.published_name, + person.published_name, ] ) - if changed_assignment.person.sessions.scheduled.count == 0 - fully_dropped.append [changed_assignment.person.published_name] - end end end end + end - fully_dropped.uniq.each do |name| - participants_fully_dropped.append_row(name) + def check_status_change(change:, to:, live: false) + return unless change[:changes]['status'] + return if ignore_session_status_change?(change: change) + + if session_status_change_to_drop?(change: change) + if live + return unless change[:object].published_session + + live_drop(session: change[:object], sheet: @participants_add_drop) + end + + session_removed_row(@session_removed, change) + + return end - send_data workbook.read_string, - filename: "ScheduleDiff-#{from.strftime('%m-%d-%Y_%H_%M')}_to_#{to.strftime('%m-%d-%Y_%H_%M')}.xlsx", - disposition: 'attachment' + if session_status_change_to_publishable?(change: change) + # Rails.logger.debug "********* STATUS CHANGE ..... ADDD #{live}" + session_added_row(@session_added, change, to) + live_add(change: change, sheet: @participants_add_drop) if live + + return + end + end + + def ignore_session_status_change?(change:) + change[:changes]['status'] && ['draft', 'dropped'].include?(change[:changes]['status'][0]) && ['draft', 'dropped'].include?(change[:changes]['status'][1]) end - def tab_headers(session_time_changed, session_room_changed, session_title_changed, session_description_changed, session_added, session_removed, participants_add_drop, participants_fully_dropped) - session_time_changed.append_row(['Session Title','Original Start Time', 'New Start Time']) - session_room_changed.append_row(['Session Title','Original Room', 'New Room']) - session_title_changed.append_row(['Original Session Title', 'New Session Title']) - session_description_changed.append_row(['Session Title','Original Description', 'New Description']) - session_added.append_row(['Session Title', 'Session Description', 'Format', 'Areas', 'Start Time', 'Room', 'Moderators', 'Participants']) - session_removed.append_row(['Session Title']) + def session_status_change_to_publishable?(change:) + change[:changes]['status'] && ['draft', 'dropped'].include?(change[:changes]['status'][0]) && ['reviewed', 'revised'].include?(change[:changes]['status'][1]) + end - participants_add_drop.append_row(['Session Title', 'Participant Dropped', 'Participant Added']) - participants_fully_dropped.append_row(['Participant Dropped']) + def session_status_change_to_drop?(change:) + change[:changes]['status'] && ['draft', 'dropped'].include?(change[:changes]['status'][1]) && ['reviewed', 'revised'].include?(change[:changes]['status'][0]) end - def live_add(session:, sheet:) - session.participant_assignments.each do |sa| + def init_sheets(workbook:) + @session_time_changed = workbook.add_worksheet("Session Time Changed") + @session_room_changed = workbook.add_worksheet("Session Room Changed") + @session_title_changed = workbook.add_worksheet("Session Title Changed") + @session_description_changed = workbook.add_worksheet("Session Description Changed") + @session_added = workbook.add_worksheet("Sessions Add") + @session_removed = workbook.add_worksheet("Sessions Removed") + @participants_fully_dropped = workbook.add_worksheet("Participants Dropped") + @participants_add_drop = workbook.add_worksheet("Participants Add Drop") + + @date_time_style = workbook.number_format(EXCEL_NBR_FORMAT) + tab_headers + end + + def tab_headers + @session_time_changed.append_row(['Session Title','Original Start Time', 'New Start Time']) + @session_room_changed.append_row(['Session Title','Original Room', 'New Room']) + @session_title_changed.append_row(['Original Session Title', 'New Session Title']) + @session_description_changed.append_row(['Session Title','Original Description', 'New Description']) + @session_added.append_row(['Session Title', 'Session Description', 'Format', 'Areas', 'Start Time', 'Room', 'Moderators', 'Participants']) + @session_removed.append_row(['Session Title']) + + @participants_add_drop.append_row(['Session Title', 'Participant Dropped', 'Participant Added']) + @participants_fully_dropped.append_row(['Participant Dropped']) + end + + def live_add(change:, sheet:) + object = change[:item_type].constantize.find change[:item_id] + return unless object.start_time && object.room_id + + object.participant_assignments.each do |sa| sheet.append_row( [ - session.title, + object.title, '', sa.person.published_name, ] @@ -242,7 +315,7 @@ def session_removed_row(sheet, change) ) end - def session_time_change_row(sheet, change, date_time_style) + def session_time_change_row(sheet, change) orig_time = Time.parse(change[:changes]['start_time'][0]) if change[:changes]['start_time'][0] new_time = Time.parse(change[:changes]['start_time'][1]) if change[:changes]['start_time'][1] # Rails.logger.debug "********* #{change[:changes]['start_time'][1]} => #{new_time.class} #{new_time.strftime("%H")}" @@ -253,31 +326,38 @@ def session_time_change_row(sheet, change, date_time_style) new_time ? FastExcel.date_num(new_time, new_time.in_time_zone.utc_offset) : nil ], [ - nil, date_time_style, date_time_style + nil, @date_time_style, @date_time_style ] ) end # Generate a row for sessions added - def session_added_row(sheet, change, date_time_style) + def session_added_row(sheet, change, to) moderator = SessionAssignmentRoleType.find_by(name: 'Moderator') participant = SessionAssignmentRoleType.find_by(name: 'Participant') - # If this is Session and it is added then so are the people + # If it is an add and we have the reify before the time and room get the latest version of the object + object = ChangeService.session_as_of(session_id: change[:item_id], to: to) + return unless object.start_time && object.room_id + + # Get the assignments at the time of change + assignments = ChangeService.assignments_as_of(session_id: change[:item_id], to: to) sheet.append_row( [ - change[:object].title, - change[:object].description, - change[:object].format&.name, - change[:object].areas.collect(&:name).join("; "), - FastExcel.date_num(change[:object].start_time, change[:object].start_time.in_time_zone.utc_offset), - change[:object].room&.name, - change[:object].participant_assignments.where("session_assignment_role_type_id = ?", moderator).collect{|a| a.person.published_name}.join("; "), - change[:object].participant_assignments.where("session_assignment_role_type_id = ?", participant).collect{|a| a.person.published_name}.join("; ") + object.title, + object.description, + object.format&.name, + object.areas.collect(&:name).join("; "), + FastExcel.date_num(object.start_time, object.start_time.in_time_zone.utc_offset), + object.room&.name, + assignments[:moderators].collect{|a| a.person.published_name}.join("; "), + assignments[:participants].collect{|a| a.person.published_name}.join("; ") + # object.participant_assignments.where("session_assignment_role_type_id = ?", moderator).collect{|a| a.person.published_name}.join("; "), + # object.participant_assignments.where("session_assignment_role_type_id = ?", participant).collect{|a| a.person.published_name}.join("; ") ], [ - nil, nil, nil, nil, date_time_style, nil, nil, nil + nil, nil, nil, nil, @date_time_style, nil, nil, nil ] ) end diff --git a/app/controllers/schedule_controller.rb b/app/controllers/schedule_controller.rb index 47f8b9717..6c3d7581a 100644 --- a/app/controllers/schedule_controller.rb +++ b/app/controllers/schedule_controller.rb @@ -2,6 +2,13 @@ class ScheduleController < ApplicationController skip_before_action :check_up, :authenticate_person!, only: [:index, :participants] + # Put in a memory cache and cache header for the conclar data + before_action :setup_cache_header + + # Cache index and particpant request for 10 minutes and prevent dog pile by allowing 1 minute regen + caches_action :index, expires_in: 10.minute, race_condition_ttl: 1.minute + caches_action :participants, expires_in: 10.minute, race_condition_ttl: 1.minute + # 1. If prod always use the published schedule # 2. If staging or dev use published - if no published then use the live for testing # 3. cache mechanism (cache can be popultaed as part of the publish) @@ -35,4 +42,8 @@ def participants content_type: 'application/json' end end + + def setup_cache_header + response.headers["Cache-Control"] = "public, max-age=#{10.minutes.to_i}" + end end diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb new file mode 100644 index 000000000..e6b84107a --- /dev/null +++ b/app/helpers/integrations_helper.rb @@ -0,0 +1,2 @@ +module IntegrationsHelper +end diff --git a/app/javascript/administration/admin_component.vue b/app/javascript/administration/admin_component.vue index ca2206691..d628ac732 100644 --- a/app/javascript/administration/admin_component.vue +++ b/app/javascript/administration/admin_component.vue @@ -50,6 +50,9 @@ ref="configurations-manager" > + + + import AdminAccordion from './admin_accordion.vue' import PersonAdd from '../people/person_add.vue'; -import ChangeUserConventionRoles from './change-user-con-roles'; -import MailingsManager from '../mailings/mailings_manager'; -import ConfigurationsManager from '../configurations/configurations_manager'; +import ChangeUserConventionRoles from './change-user-con-roles.vue'; +import MailingsManager from '../mailings/mailings_manager.vue'; +import ConfigurationsManager from '../configurations/configurations_manager.vue'; import SheetImporterVue from '../components/sheet_importer_vue.vue'; -import AgreementManager from "@/agreements/agreement_manager"; +import AgreementManager from "@/agreements/agreement_manager.vue"; import ScheduleSettings from "@/schedule/schedule_settings.vue"; +import IntegrationSettings from "@/integrations/integration_settings.vue" export default { components: { @@ -79,7 +83,8 @@ export default { SheetImporterVue, MailingsManager, ConfigurationsManager, - ScheduleSettings + ScheduleSettings, + IntegrationSettings, }, name: 'AdminComponent', data: () => ({ diff --git a/app/javascript/airmeet/airmeet_settings.vue b/app/javascript/airmeet/airmeet_settings.vue new file mode 100644 index 000000000..d24dfba94 --- /dev/null +++ b/app/javascript/airmeet/airmeet_settings.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/app/javascript/app.router.js b/app/javascript/app.router.js index 6fa75824b..51c44b9e9 100644 --- a/app/javascript/app.router.js +++ b/app/javascript/app.router.js @@ -98,6 +98,17 @@ Vue.use(VueRouter); var con_roles=[], isAdmin=false, hasPowers=false; export const router = new VueRouter({ + scrollBehavior(to) { + console.log(to) + if (to.hash) { + setTimeout(() => { + const element = document.getElementById(to.hash.replace(/#/, '')) + if (element && element.scrollIntoView) { + element.scrollIntoView({block: 'start'}) + } + }, 10) + } + }, routes: [ { path: '/login', diff --git a/app/javascript/integrations/integration_settings.vue b/app/javascript/integrations/integration_settings.vue new file mode 100644 index 000000000..3bb15c8cc --- /dev/null +++ b/app/javascript/integrations/integration_settings.vue @@ -0,0 +1,28 @@ + + + + + diff --git a/app/javascript/people/detail.vue b/app/javascript/people/detail.vue index 4b7c0f68e..42bf0f651 100644 --- a/app/javascript/people/detail.vue +++ b/app/javascript/people/detail.vue @@ -31,11 +31,11 @@
Can Share:
{{ selected.can_share ? 'Y' : 'N' }}
Can Photo:
-
{{ selected.can_photo ? 'Y' : 'N' }}
+
{{ selected.can_photo === "yes" ? 'Y' : 'N' }}
Can Stream:
-
{{ selected.can_stream ? 'Y' : 'N' }}
+
{{ selected.can_stream === "yes" ? 'Y' : 'N' }}
Can Record:
-
{{ selected.can_stream ? 'Y' : 'N' }}
+
{{ selected.can_record === "yes" ? 'Y' : 'N' }}
diff --git a/app/javascript/people/person_tabs.vue b/app/javascript/people/person_tabs.vue index 90ac4467b..c902d47e5 100644 --- a/app/javascript/people/person_tabs.vue +++ b/app/javascript/people/person_tabs.vue @@ -53,6 +53,9 @@ + +
{{JSON.stringify(person.integrations, undefined, 2)}}
+
@@ -132,6 +135,9 @@ export default { baseTabs.push('email'); baseTabs.push('admin'); } + if(this.currentUserIsAdmin) { + baseTabs.push('integrations'); + } return baseTabs; }, person() { diff --git a/app/javascript/reports/reports_screen.vue b/app/javascript/reports/reports_screen.vue index 669d09a9d..ba99ee414 100644 --- a/app/javascript/reports/reports_screen.vue +++ b/app/javascript/reports/reports_screen.vue @@ -2,11 +2,11 @@

These are interim reports.

Participants
    @@ -182,14 +182,6 @@ Session data included: all sessions

    -
  • - Daily Grid -

    - Description: Session information for daily grid, one line per session
    - Fields: Session title, area(s) of session, session format, session start time, session duration, session room
    - Session data included: all scheduled sessions that are visible and published -

    -
  • Scheduled Sessions Streamed and/or Recorded

    @@ -289,7 +281,7 @@ Schedule by Participant

    Description: Schedule for each participant, one line per person and session
    - Fields: Person name, publishes name, participant status, session title, area(s) of session, session start time, room, moderator (y/n), invisible participant (y/n)
    + Fields: Person name, published name, participant status, session title, area(s) of session, session start time, room, moderator (y/n), invisible participant (y/n)
    Person data included: moderators, participants, invisible participants

  • @@ -314,6 +306,23 @@ Person data included: moderators, participants, invisible participants

    +
  • + Daily Grid +

    + Description: Session information for daily grid, one line per session
    + Fields: Session title, area(s) of session, session format, session start time, session duration, session room
    + Session data included: all scheduled sessions that are visible and published +

    +
  • +
  • + Participant Emails and Virtual Sessions +

    + Description: List of emails for moderators and participants of published sessions, with indicator about whether they are assigned to any session(s) of environment virtual, one line per participant
    + Fields: Published name, name, whether person is assigned to a virtual session (y∕n), primary email, other emails
    + Session data included: all published sessions
    + Person data included: moderators and participants +

    +
diff --git a/app/javascript/sessions/session_tabs.vue b/app/javascript/sessions/session_tabs.vue index 5b7c53d27..f1928dc5d 100644 --- a/app/javascript/sessions/session_tabs.vue +++ b/app/javascript/sessions/session_tabs.vue @@ -30,6 +30,9 @@ + +
{{JSON.stringify(published_session.integrations, undefined, 2)}}
+
@@ -48,6 +51,8 @@ import SessionSchedule from './session_schedule'; import SessionConflicts from '../conflicts/session_conflicts.vue' import { sessionConflictModel } from '@/store/session_conflict.store' import settingsMixin from "@/store/settings.mixin"; +import { personSessionMixin } from '@/mixins'; +import { publishedSessionModel } from '@/store/published_session.store'; export default { name: "SessionTabs", @@ -65,16 +70,20 @@ export default { }, mixins: [ modelUtilsMixin, - settingsMixin + settingsMixin, + personSessionMixin ], data: () => ({ sessionAssignmentModel, - sessionConflictModel + sessionConflictModel, }), computed: { session() { return this.selected_model(sessionModel); }, + published_session() { + return this.selected_model(publishedSessionModel) || {}; + }, assignmentFilter() { let filter = { "op": "all", @@ -120,6 +129,9 @@ export default { case 4: path = `notes/${this.id}`; break; + case 5: + path = `integrations/${this.id}` + break; } // console.debug("****** Path:", path) // change the router path to match the current tab @@ -138,7 +150,10 @@ export default { (obj) => { this.select_model(sessionModel, obj); } - ) + ), + this.fetch_model_by_id(publishedSessionModel, this.id).then( (obj) => { + this.select_model(publishedSessionModel, obj) + }) } } diff --git a/app/javascript/store/integration.store.js b/app/javascript/store/integration.store.js new file mode 100644 index 000000000..c00767a21 --- /dev/null +++ b/app/javascript/store/integration.store.js @@ -0,0 +1,28 @@ +import { FETCH } from "./model.store" + +export const integrationModel = 'integration' + +export const integrationEndpoints = { + [integrationModel]: 'integration' +} + +export const FETCH_AIRMEET_INTEGRATION = 'FETCH AIRMEET INTEGRATION' +export const SET_AIRMEET_INTEGRATION = 'SET AIRMEET INTEGRATION' + +export const integrationStore = { + state: { + airmeet: {} + }, + mutations: { + [SET_AIRMEET_INTEGRATION] (state, integration) { + state.airmeet = integration; + } + }, + actions: { + [FETCH_AIRMEET_INTEGRATION] ({dispatch, commit}) { + dispatch(FETCH, {url: 'integration/airmeet'}).then(data => { + commit(SET_AIRMEET_INTEGRATION, data); + }) + } + } +} diff --git a/app/javascript/store/model.store.js b/app/javascript/store/model.store.js index 677963979..3f7939da2 100644 --- a/app/javascript/store/model.store.js +++ b/app/javascript/store/model.store.js @@ -75,9 +75,11 @@ import { configurationStore, configurationEndpoints } from './configuration.stor // session add-ons import { sessionAssignmentStore, sessionAssignmentEndpoints } from './session_assignment.store'; +import { publishedSessionEndpoints, publishedSessionStore } from './published_session.store'; // global app things import { appStore } from './app.store'; +import { integrationEndpoints, integrationStore } from './integration.store'; // schedule workflow import { scheduleWorkflowStore, scheduleWorkflowEndpoints } from './schedule_workflow/schedule_workflow.store'; @@ -106,6 +108,8 @@ const endpoints = { // ...personExclusionEndpoints, ...scheduleWorkflowEndpoints, ...personScheduleApprovalEndpoints, + ...publishedSessionEndpoints, + ...integrationEndpoints, } // NOTE: this is really the store @@ -142,6 +146,7 @@ export const store = new Vuex.Store({ ...sessionConflictStore.selected, ...formatStore.selected, ...personScheduleApprovalStore.selected, + ...publishedSessionStore.selected, }, ...personSessionStore.state, ...settingsStore.state, @@ -153,6 +158,7 @@ export const store = new Vuex.Store({ ...roomStore.state, ...appStore.state, ...scheduleWorkflowStore.state, + ...integrationStore.state, // ...mailingStore.state }, getters: { @@ -211,7 +217,8 @@ export const store = new Vuex.Store({ ...surveyStore.mutations, ...searchStateStore.mutations, ...roomStore.mutations, - ...appStore.mutations + ...appStore.mutations, + ...integrationStore.mutations }, actions: { /** @@ -381,5 +388,6 @@ export const store = new Vuex.Store({ ...sessionConflictStore.actions, ...scheduleWorkflowStore.actions, ...personScheduleApprovalStore.actions, + ...integrationStore.actions, } }) diff --git a/app/javascript/store/published_session.store.js b/app/javascript/store/published_session.store.js new file mode 100644 index 000000000..2acf4d994 --- /dev/null +++ b/app/javascript/store/published_session.store.js @@ -0,0 +1,11 @@ +export const publishedSessionModel = 'published_session' + +export const publishedSessionEndpoints = { + [publishedSessionModel]: 'published_session' +} + +export const publishedSessionStore = { + selected: { + [publishedSessionModel]: null + } +} diff --git a/app/models/integration.rb b/app/models/integration.rb new file mode 100644 index 000000000..66c9978d0 --- /dev/null +++ b/app/models/integration.rb @@ -0,0 +1,2 @@ +class Integration < ApplicationRecord +end diff --git a/app/policies/integration_policy.rb b/app/policies/integration_policy.rb new file mode 100644 index 000000000..85e09549d --- /dev/null +++ b/app/policies/integration_policy.rb @@ -0,0 +1,19 @@ +class IntegrationPolicy < PlannerPolicy + def publish? + allowed?(action: :publish) + end + + def airmeet? + allowed?(action: :airmeet) + end + + def update? + allowed?(action: :update) + end + + class Scope < PlannerPolicy::Scope + def resolve + scope.all + end + end +end diff --git a/app/policies/published_session_policy.rb b/app/policies/published_session_policy.rb new file mode 100644 index 000000000..16736953f --- /dev/null +++ b/app/policies/published_session_policy.rb @@ -0,0 +1,7 @@ +class PublishedSessionPolicy < PlannerPolicy + class Scope < PlannerPolicy::Scope + def resolve + scope.all + end + end +end diff --git a/app/policies/reports/program_ops_report_policy.rb b/app/policies/reports/program_ops_report_policy.rb index d573b7655..79b305b17 100644 --- a/app/policies/reports/program_ops_report_policy.rb +++ b/app/policies/reports/program_ops_report_policy.rb @@ -2,4 +2,8 @@ class Reports::ProgramOpsReportPolicy < BasePolicy def back_of_badge? allowed?(action: :back_of_badge) end + + def virtual_people? + allowed?(action: :virtual_people) + end end diff --git a/app/serializers/conclar/session_serializer.rb b/app/serializers/conclar/session_serializer.rb index 4d7b4c418..295644063 100644 --- a/app/serializers/conclar/session_serializer.rb +++ b/app/serializers/conclar/session_serializer.rb @@ -89,7 +89,7 @@ class Conclar::SessionSerializer < ActiveModel::Serializer if object.streamed t = { value: "session_streamed", - category: "Note", + category: "Environment", label: "Streamed" } res << t diff --git a/app/serializers/integration_serializer.rb b/app/serializers/integration_serializer.rb new file mode 100644 index 000000000..0120b3c9b --- /dev/null +++ b/app/serializers/integration_serializer.rb @@ -0,0 +1,15 @@ +class IntegrationSerializer + include JSONAPI::Serializer + + attributes :lock_version, :created_at, :updated_at, + :name, :id + + attribute :config do |integration| + {airmeet_id: integration.config["airmeet_id"], + airmeet_host: integration.config["airmeet_host"] + } + end + + + +end diff --git a/app/serializers/person_serializer.rb b/app/serializers/person_serializer.rb index f58785557..f757ac4c5 100644 --- a/app/serializers/person_serializer.rb +++ b/app/serializers/person_serializer.rb @@ -41,7 +41,8 @@ class PersonSerializer #< ActiveModel::Serializer :timezone, :twelve_hour, :attendance_type, - :availability_notes + :availability_notes, + :integrations # status and comments hidden except for staff protected_attributes :con_state, :comments diff --git a/app/serializers/published_session_serializer.rb b/app/serializers/published_session_serializer.rb index e7bffc08a..4150e7cb1 100644 --- a/app/serializers/published_session_serializer.rb +++ b/app/serializers/published_session_serializer.rb @@ -1,5 +1,5 @@ class PublishedSessionSerializer include JSONAPI::Serializer - attributes :id, :lock_version, :created_at, :updated_at + attributes :id, :lock_version, :created_at, :updated_at, :integrations end diff --git a/app/serializers/room_serializer.rb b/app/serializers/room_serializer.rb index a675c0b05..772c3ee93 100644 --- a/app/serializers/room_serializer.rb +++ b/app/serializers/room_serializer.rb @@ -5,7 +5,7 @@ class RoomSerializer :name, :sort_order, :purpose, :comment, :capacity, :floor, :open_for_schedule, :is_virtual, :area_of_space, :length, :width, :height, - :venue_id, :room_set_id + :venue_id, :room_set_id, :integrations end diff --git a/app/services/airmeet_api_service.rb b/app/services/airmeet_api_service.rb new file mode 100644 index 000000000..16ed3e1c8 --- /dev/null +++ b/app/services/airmeet_api_service.rb @@ -0,0 +1,176 @@ +module AirmeetApiService + def self.integration + @integration ||= Integration.find_or_create_by({name: :airmeet}) + end + + def self.config + integration.config + end + + def self.token + if !config['token'] || Time.at(config['token_exp']) < Time.now + auth + else + config['token'] + end + end + + def self.airmeet_id + config['airmeet_id'] + end + + def self.airmeet + @airmeet ||= Airmeet.new + end + + def self.get_participants + Airmeet.get("/airmeet/#{airmeet_id}/participants") + end + + def self.auth + token = airmeet.auth["token"] + integration.update({config: config.merge({ + token: token, + token_exp: (Time.now + (29 * 24 * 60 * 60)).to_i + })}) + token + end + + def self.info + Airmeet.get("/airmeet/#{airmeet_id}/info") + end + + def self.get_session(id) + info["sessions"].find { |s| s["sessionid"] == id } + end + + def self.create_session(sessionTitle:, sessionStartTime:, sessionDuration:, sessionSummary:, hostEmail:, speakerEmails: [], cohostEmails: []) + sessionStartTime = sessionStartTime.to_i * 1000 + Airmeet.post("/airmeet/#{airmeet_id}/session", { + body: { + sessionTitle: sessionTitle, + sessionStartTime: sessionStartTime, + sessionDuration: sessionDuration, + sessionSummary: sessionSummary, + hostEmail: hostEmail, + speakerEmails: speakerEmails, + cohostEmails: cohostEmails, + type: "HOSTING" + }.to_json + }) + end + + def self.create_speaker(name:, email:, organisation: "", designation: "", imageUrl: "", bio: "", city: "", country: "") + Airmeet.post("/airmeet/#{airmeet_id}/speaker", { + body: { + name: name, + email: email, + organisation: organisation, + designation: designation, + imageUrl: imageUrl, + bio: bio || " ", + city: city, + country: country, + }.to_json + }) + end + + def self.person_to_airmeet(person) + speaker_email = person.primary_email.email + country = nil + city = nil + if person.integrations["airmeet"] + speaker_email = person.integrations["airmeet"]["speaker_email"] || speaker_email + country = person.integrations["airmeet"]["country"] + city = person.integrations["airmeet"]["city"] + end + name = person.published_name + bio = person.bio + args = {name: name, email: speaker_email, bio: bio} + if country + args[:country] = country + end + if city + args[:city] = city + end + puts args + result = create_speaker(args) + puts result + person.update({integrations: person.integrations.merge({airmeet: {speaker_email: speaker_email, synced: true, data: args, synced_at: Time.now.iso8601}})}) + result["email"] + end + + def self.session_to_airmeet(session) + args = {sessionTitle: "#{session.room.name} - #{session.title} - #{session.format.name} - #{session.area_list.join(", ")}", + sessionSummary: session.description, + sessionDuration: session.duration, + sessionStartTime: session.start_time, + hostEmail: room_hosts[session.room_id] + }; + participants = session.published_session_assignments.filter { |sa| sa.session_assignment_role_type_id == moderator_id || sa.session_assignment_role_type_id == participant_id }.map { |sa| sa.person } + if session.environment == "virtual" + args[:speakerEmails] = participants.map{|p| p.integrations["airmeet"]["speaker_email"]} + args[:cohostEmails] = session.published_session_assignments.filter { |sa| sa.session_assignment_role_type_id == moderator_id }.map { |sa| sa.person.integrations["airmeet"]["speaker_email"] } + end + puts args + result = create_session(args); + puts result + session.update({integrations: session.integrations.merge({airmeet: {session_id: result["uid"], synced: true, synced_at: Time.now(), data: args}})}) + if session.environment == "virtual" + people_tokens = result["token"].inject({}) {|p,c| p[c["email"]] = c["token"]; p} + participants.each { |p| p.update({integrations: p.integrations.merge({airmeet: (p.integrations["airmeet"] || {}).merge({token: people_tokens[(p.integrations["airmeet"] || {})["speaker_email"]]})})})} + end + end + + def self.room_hosts + @room_hosts ||= Room.where.not(integrations: {}).inject({}) { |p, c| p[c.id] = (c.integrations["airmeet"] || {})["room_host_email"]; p } + end + + def self.virtual_people + Person.left_outer_joins(:published_session_assignments, :published_sessions, :primary_email) + .where(published_sessions: { environment: 'virtual' }, published_session_assignments: {session_assignment_role_type_id: [moderator_id, participant_id]}) + .distinct + end + + def self.virtual_sessions + SessionService.published_sessions.where(streamed: true) + end + + def self.moderator_id + @moderator_id = SessionAssignmentRoleType.find_by(name: 'Moderator').id + end + + def self.participant_id + @participant_id = SessionAssignmentRoleType.find_by(name: 'Participant').id + end + + def self.sync_to_airmeet + virtual_people.map { |p| person_to_airmeet(p) } + virtual_sessions.map { |s| session_to_airmeet(s) } + puts "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA IT WORKED" + end + + class Airmeet + include HTTParty + base_uri "https://api-gateway.airmeet.com/prod" + + headers 'Content-Type' => 'application/json' + + # TODO fix me for not testing + default_options.update(verify: false) + + def auth + self.class.post("/auth", { + headers: { + "X-Airmeet-Access-Key": ENV["AIRMEET_ACCESS_KEY"], + "X-Airmeet-Secret-Key": ENV["AIRMEET_SECRET_KEY"], + "X-Airmeet-Access-Token": "" + }, + verify: false + }) + end + + headers 'X-Airmeet-Access-Token' => AirmeetApiService.token + end + +end diff --git a/app/services/change_service.rb b/app/services/change_service.rb index 1d5921d99..ddf8bc78d 100644 --- a/app/services/change_service.rb +++ b/app/services/change_service.rb @@ -16,6 +16,73 @@ def self.published_changes(from:, to: nil) } end + def self.dropped_people(from:, to: nil) + res = [] + changes = get_changes(clazz: Audit::PersonVersion, type: Person, from: from, to: to) + changes.each do |id, change| + if change[:changes]['con_state'] && ['declined', 'rejected'].include?(change[:changes]['con_state'][1] ) + # do not count a "dropped" state to another dropped state + next if ['declined', 'rejected'].include?(change[:changes]['con_state'][0]) + + res.append [change[:object].published_name] + end + end + res.uniq + end + + def self.session_as_of(session_id:, to:) + self.object_as_of(audit: Audit::SessionVersion, item_id: session_id, item_type: 'Session', to: to) + end + + def self.object_as_of(audit:, item_id:, item_type:, to:) + object_version = audit.where("item_id = ? and item_type = ? and created_at <= ?", item_id, item_type, to) + .order('created_at desc') + .first + return nil unless object_version + + object = object_version.reify + return object + end + + def self.assignments_as_of(session_id:, to:) + moderator = SessionAssignmentRoleType.find_by(name: 'Moderator') + participant = SessionAssignmentRoleType.find_by(name: 'Participant') + + participants = self.assignments_for(session_id: session_id, role_id: participant.id, to: to) + moderators = self.assignments_for(session_id: session_id, role_id: moderator.id, to: to) + + { + participants: participants, + moderators: moderators + } + end + + def self.assignments_for(session_id:, role_id:, to:) + audits = Audit::SessionVersion + .where_object(session_id: session_id) + .where("created_at <= ?", to) + .order("created_at desc") + grouped_audits = audits.group_by {|a| a.item_id} + + res = [] + grouped_audits.each do |key, item_audits| + change = self.comnbined_changes(item_audits: item_audits, type: SessionAssignment) + res.concat [change[:object]] if self.assigned?(change: change, role_id: role_id) + end + + res + end + + def self.assigned?(change:, role_id:) + return false unless change[:object] + + return true if !change[:changes]['session_assignment_role_type_id'] && change[:object].session_assignment_role_type_id == role_id + + return true if change[:changes]['session_assignment_role_type_id'] && change[:changes]['session_assignment_role_type_id'][1] == role_id + + return false + end + def self.sessions_changed(from:, to: nil) get_changes(clazz: Audit::SessionVersion, type: Session, from: from, to: to) end @@ -47,29 +114,40 @@ def self.get_changes(clazz:, type:, from:, to:, publishable_session_ids: nil) audits = audits.where("created_at >= ?", from) if from audits = audits.where("created_at <= ?", to) if to - # Rails.logger.debug "**** AUDITS #{audits.count}" - grouped_audits = audits.group_by {|a| a.item_id} grouped_audits.each do |key, item_audits| # Rails.logger.debug "**** AUDIT #{key} #{publishable_session_ids}" # just in case we sort by date - item_audits.sort{|a,b| a.created_at <=> b.created_at}.each do |audit| - # merge the change history - if changes[key] - changes[key][:changes] = self.merge_change_set(to: changes[key][:changes], from: audit.object_changes) - else - obj = type.find(audit.item_id) if audit.event != 'destroy' && type.exists?(audit.item_id) - obj ||= audit.reify - if publishable_session_ids - next unless publishable_session_ids.include?(obj.session_id) - end - changes[key] = {event: audit.event, object: obj, changes: audit.object_changes} - end + change = self.comnbined_changes(item_audits: item_audits, type: type) + if publishable_session_ids && change[:object].respond_to?(:session_id) + next unless publishable_session_ids.include?(change[:object].session_id) + end + + changes[key] = change + end + + changes + end + + def self.comnbined_changes(item_audits:, type:) + changes = nil + + item_audits.sort{|a,b| a.created_at <=> b.created_at}.each do |audit| + # merge the change history + if changes + changes[:changes] = self.merge_change_set(to: changes[:changes], from: audit.object_changes) + else + # Get the old version of the object + obj = if audit.event == 'create' + type.find(audit.item_id) if type.exists?(audit.item_id) + else + audit.reify + end + changes = {item_id: audit.item_id, item_type: audit.item_type, event: audit.event, object: obj, changes: audit.object_changes} end end - # TODO: any way we can order by the session title ??? changes end diff --git a/config/environments/production.rb b/config/environments/production.rb index 9563dfb53..9d45caf19 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -55,6 +55,10 @@ # Use a different cache store in production. # config.cache_store = :mem_cache_store + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{10.minutes.to_i}" + } # Use a real queuing backend for Active Job (and separate queues per environment). config.active_job.queue_adapter = :sidekiq diff --git a/config/environments/staging.rb b/config/environments/staging.rb index d1b743c60..1bb4f439b 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -55,6 +55,10 @@ # Use a different cache store in staging. # config.cache_store = :mem_cache_store + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => "public, max-age=#{10.minutes.to_i}" + } # Use a real queuing backend for Active Job (and separate queues per environment). config.active_job.queue_adapter = :sidekiq diff --git a/config/routes.rb b/config/routes.rb index a9ec66af6..c6d9f4522 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -90,10 +90,13 @@ get 'report/people_reports/moderators', to: 'reports/people_reports#moderators' get 'report/program_ops_reports/back_of_badge', to: 'reports/program_ops_reports#back_of_badge' + get 'report/program_ops_reports/virtual_people', to: 'reports/program_ops_reports#virtual_people' get 'report/schedule_reports/schedule_diff(/:from)(/:to)', to: 'reports/schedule_reports#schedule_diff' get 'publication_date/reset', to: 'publication_dates#reset' - resources :publication_dates, path: 'publication_date', only: [:index] + resources :publication_dates, path: 'publication_date', only: [:index, :update] + resources :integrations, path: 'integration', only: [:index, :update] + get 'integration/airmeet', to: 'integrations#airmeet' resources :availabilities, path: 'availability', except: [:index] resources :person_exclusions, path: 'person_exclusion', except: [:index] diff --git a/db/integrations.md b/db/integrations.md new file mode 100644 index 000000000..5df98effa --- /dev/null +++ b/db/integrations.md @@ -0,0 +1,57 @@ +This page should document what data structrue is expected for the integration columns + +## Config + + { + "airmeet_id": "uuid", + "airmeet_host": "host email", + "active": true, + "token": "jwt set by the system here" + "token_exp": "time set by the system here" + } + +## Session + + { + "airmeet": { + "synced": true + "synced_at": "ISO8601 timestamp", + "sessionTitle": "generated string here" , + "sesionStartTime": "epoch in ms", + "sessionDuration": "number in minutes", + "sessionSummary": "generated string here" + "hostEmail": "room email", + "speakerEmails": "speaker emails", + "cohostEmails": "moderator emails", + "sessionId": "??" + } + } + +## Person + + { + "airmeet": { + "synced": true + "speaker_email": "speaker@email.com" + "bio": "bio here", + "name": "name here" + "synced_at": "ISO8601 timestamp" + } + } + +## Session Assignment + + { + "airmeet": { + "magic_link": "here" + "synced_at": "ISO8601 timestamp" + } + } + +## Room + + { + "airmeet": { + "room_host_email": "helpdesk+roomname@chicon.org" + } + } diff --git a/db/migrate/20220818200500_create_integrations.rb b/db/migrate/20220818200500_create_integrations.rb new file mode 100644 index 000000000..0f9e91fc2 --- /dev/null +++ b/db/migrate/20220818200500_create_integrations.rb @@ -0,0 +1,27 @@ +class CreateIntegrations < ActiveRecord::Migration[6.1] + def change + create_table :integrations, id: :uuid do |t| + t.string :name + t.jsonb :config, null: false, default: {} + + t.timestamps + end + + create_table :integration_publishes, id: :uuid do |t| + t.string :integration_name + t.jsonb :data, null: false, default: {} + t.datetime :started_at + t.datetime :completed_at + t.string :created_by + + t.timestamps + end + + # fields to hold the integration information for each session/person/session_assignment/room + add_column :published_sessions, :integrations, :jsonb, null: false, default: {} + add_column :people, :integrations, :jsonb, null: false, default: {} + # todo maybe i don't need this one if the tokens are always the same + add_column :published_session_assignments, :integrations, :jsonb, null: false, default: {} + add_column :rooms, :integrations, :jsonb, null: false, default: {} + end +end diff --git a/db/migrate/20220821001724_add_lock_version_to_integrations.rb b/db/migrate/20220821001724_add_lock_version_to_integrations.rb new file mode 100644 index 000000000..842c72b2f --- /dev/null +++ b/db/migrate/20220821001724_add_lock_version_to_integrations.rb @@ -0,0 +1,5 @@ +class AddLockVersionToIntegrations < ActiveRecord::Migration[6.1] + def change + add_column :integrations, :lock_version, :integer, default: 0 + end +end diff --git a/db/structure.sql b/db/structure.sql index c6ec3094f..4d3757d1c 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -616,7 +616,8 @@ END) STORED, attendance_type character varying(200) DEFAULT NULL::character varying, twelve_hour boolean DEFAULT true, timezone character varying(500) DEFAULT NULL::character varying, - availability_notes character varying + availability_notes character varying, + integrations jsonb DEFAULT '{}'::jsonb NOT NULL ); @@ -926,6 +927,36 @@ CREATE TABLE public.ignored_conflicts ( ); +-- +-- Name: integration_publishes; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.integration_publishes ( + id uuid DEFAULT public.gen_random_uuid() NOT NULL, + integration_name character varying, + data jsonb DEFAULT '{}'::jsonb NOT NULL, + started_at timestamp without time zone, + completed_at timestamp without time zone, + created_by character varying, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL +); + + +-- +-- Name: integrations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.integrations ( + id uuid DEFAULT public.gen_random_uuid() NOT NULL, + name character varying, + config jsonb DEFAULT '{}'::jsonb NOT NULL, + created_at timestamp(6) without time zone NOT NULL, + updated_at timestamp(6) without time zone NOT NULL, + lock_version integer DEFAULT 0 +); + + -- -- Name: label_dimensions; Type: TABLE; Schema: public; Owner: - -- @@ -1123,7 +1154,11 @@ CREATE VIEW public.person_schedules AS sessions.format_id, sessions.participant_notes, sessions.description, - sessions.environment + sessions.environment, + CASE + WHEN (sa.updated_at > sessions.updated_at) THEN sa.updated_at + ELSE sessions.updated_at + END AS updated_at FROM (((public.session_assignments sa JOIN public.session_assignment_role_type sart ON (((sart.id = sa.session_assignment_role_type_id) AND (sart.role_type = 'participant'::public.assignment_role_enum) AND ((sart.name)::text <> 'Reserve'::text)))) JOIN public.people p ON ((p.id = sa.person_id))) @@ -1347,7 +1382,9 @@ CREATE TABLE public.publication_dates ( dropped_sessions integer DEFAULT 0, new_assignments integer DEFAULT 0, updated_assignments integer DEFAULT 0, - dropped_assignments integer DEFAULT 0 + dropped_assignments integer DEFAULT 0, + sent_external boolean DEFAULT false NOT NULL, + lock_version integer DEFAULT 0 ); @@ -1392,7 +1429,8 @@ CREATE TABLE public.published_session_assignments ( session_assignment_role_type_id uuid NOT NULL, person_id uuid NOT NULL, sort_order integer, - visibility public.visibility_enum DEFAULT 'public'::public.visibility_enum + visibility public.visibility_enum DEFAULT 'public'::public.visibility_enum, + integrations jsonb DEFAULT '{}'::jsonb NOT NULL ); @@ -1421,7 +1459,8 @@ CREATE TABLE public.published_sessions ( environment public.session_environments_enum DEFAULT 'unknown'::public.session_environments_enum, minors_participation jsonb, recorded boolean DEFAULT false NOT NULL, - streamed boolean DEFAULT false NOT NULL + streamed boolean DEFAULT false NOT NULL, + integrations jsonb DEFAULT '{}'::jsonb NOT NULL ); @@ -1447,7 +1486,8 @@ CREATE TABLE public.rooms ( room_set_id uuid, length numeric, width numeric, - height numeric + height numeric, + integrations jsonb DEFAULT '{}'::jsonb NOT NULL ); @@ -2206,6 +2246,22 @@ ALTER TABLE ONLY public.ignored_conflicts ADD CONSTRAINT ignored_conflicts_pkey PRIMARY KEY (id); +-- +-- Name: integration_publishes integration_publishes_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.integration_publishes + ADD CONSTRAINT integration_publishes_pkey PRIMARY KEY (id); + + +-- +-- Name: integrations integrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.integrations + ADD CONSTRAINT integrations_pkey PRIMARY KEY (id); + + -- -- Name: label_dimensions label_dimensions_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -3369,6 +3425,9 @@ INSERT INTO "schema_migrations" (version) VALUES ('20220726130346'), ('20220801152151'), ('20220801173704'), -('20220801195644'); +('20220801195644'), +('20220818022629'), +('20220818200500'), +('20220821001724'); diff --git a/jsconfig.json b/jsconfig.json index a6c92856e..79666df62 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "target": "es6", "allowSyntheticDefaultImports": false, + "jsx": "preserve", "baseUrl": "./", "paths": { "@/*": ["app/javascript/*"], diff --git a/lib/tasks/chicon_airmeet.rake b/lib/tasks/chicon_airmeet.rake new file mode 100644 index 000000000..a9d5de663 --- /dev/null +++ b/lib/tasks/chicon_airmeet.rake @@ -0,0 +1,37 @@ +namespace :chicon do + desc "Configure airmeet for chicon" + task configure_airmeet_test: :environment do + integration = Integration.find_or_create_by({name: :airmeet}) + # this data is for testing. we are going to need the real information for the actual show here at some point. + # and/or create a ui for it but let's try this first. + # none of this gets you actual access to the airmeet so it's probably ok to actually check in??? + integration.update!({ + config: { + airmeet_id: "ac4b0bc0-1079-11ed-bed5-3112cc3e0b52", + airmeet_host: "gail.terman@chicon.org", + active: true + } + }) + + # Room.find_by({name: "Airmeet 1"}).update({integrations: {airmeet: {room_host_email: 'gail.terman@chicon.org'}}}) + end + + task configure_airmeet: :environment do + integration = Integration.find_or_create_by({name: :airmeet}) + # this data is for testing. we are going to need the real information for the actual show here at some point. + # and/or create a ui for it but let's try this first. + # none of this gets you actual access to the airmeet so it's probably ok to actually check in??? + integration.update!({ + config: { + airmeet_id: "NOT SET YET", + airmeet_host: "NOT SET YET", + active: false + } + }) + end + + task sync_airmeet: :environment do + AirmeetApiService.sync_to_airmeet + end +end + diff --git a/lib/tasks/rbac.rake b/lib/tasks/rbac.rake index cd0a0811e..4ca88238c 100644 --- a/lib/tasks/rbac.rake +++ b/lib/tasks/rbac.rake @@ -281,6 +281,9 @@ namespace :rbac do }, "publication_date": { "index": false + }, + "integration": { + "publish": false } }) end @@ -548,7 +551,8 @@ namespace :rbac do "schedule_diff": true }, "program_ops_report": { - "back_of_badge": true + "back_of_badge": true, + "virtual_people": true }, "conflict_report": { "people_outside_availability": true, @@ -589,6 +593,9 @@ namespace :rbac do }, "publication_date": { "index": true + }, + "integration": { + "publish": false } }) end @@ -866,7 +873,8 @@ namespace :rbac do "schedule_diff": true }, "program_ops_report": { - "back_of_badge": true + "back_of_badge": true, + "virtual_people": true }, "session_conflict": { "conflicts_with": true, @@ -897,6 +905,9 @@ namespace :rbac do }, "publication_date": { "index": true + }, + "integration": { + "publish": true } }) end diff --git a/spec/helpers/integrations_helper_spec.rb b/spec/helpers/integrations_helper_spec.rb new file mode 100644 index 000000000..c39979c7b --- /dev/null +++ b/spec/helpers/integrations_helper_spec.rb @@ -0,0 +1,15 @@ +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the IntegrationsHelper. For example: +# +# describe IntegrationsHelper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +RSpec.describe IntegrationsHelper, type: :helper do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/integration_spec.rb b/spec/models/integration_spec.rb new file mode 100644 index 000000000..aae8d31da --- /dev/null +++ b/spec/models/integration_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Integration, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/requests/integrations_spec.rb b/spec/requests/integrations_spec.rb new file mode 100644 index 000000000..5426a96db --- /dev/null +++ b/spec/requests/integrations_spec.rb @@ -0,0 +1,7 @@ +require 'rails_helper' + +RSpec.describe "Integrations", type: :request do + describe "GET /index" do + pending "add some examples (or delete) #{__FILE__}" + end +end