From a1a65511334c8a7011e03c647b0ed1ffb911c569 Mon Sep 17 00:00:00 2001 From: Don Restarone <35935196+donrestarone@users.noreply.github.com> Date: Thu, 14 Sep 2023 00:33:05 -0400 Subject: [PATCH] [feature] Calendar and icalendar meetings # Calendar and Meetings addresses: https://github.com/restarone/violet_rails/issues/1597 This release adds the ability for Violet to handle your calendar. Incoming emails with .ics attachments will automatically be added to your calendar as meetings. ## Calendar UI ![Screenshot from 2023-09-14 00-25-19](https://github.com/restarone/violet_rails/assets/35935196/d6ab2626-85e1-49d3-83d3-f2aadfc34eeb) ## Outgoing Meeting request RSVP controls ![IMG_7264](https://github.com/restarone/violet_rails/assets/35935196/bae81b61-32e4-4970-a9b8-60b787366f11) todo: 1. include .vcs file for outlook 2. add validations to meeting model further reading: 1. icalendar syncing events: https://joshfrankel.me/blog/lemme-pencil-you-in-using-icalendar-and-rails-to-sync-calendar-events/ 2. all the options: https://blog.corsego.com/icalendar-ruby 3. publish? https://stackoverflow.com/questions/55927263/what-does-icalendar-publish-method-do 4. dealing with email client quirkyness when displaying RSVP buttons: https://stackoverflow.com/questions/66102584/when-i-add-method-request-to-icalendar-gmail-stops-recognizing-as-event --- Gemfile | 5 +- Gemfile.lock | 7 + app/assets/stylesheets/application.scss | 1 + .../comfy/admin/calendars_controller.rb | 23 + app/controllers/meetings_controller.rb | 142 +++ app/mailboxes/e_mailbox.rb | 43 +- app/models/meeting.rb | 6 + .../comfy/admin/calendars/index.html.haml | 38 + .../admin/cms/partials/_navigation_inner.haml | 1 + app/views/meetings/_form.html.haml | 41 + app/views/meetings/_meeting.json.jbuilder | 2 + app/views/meetings/edit.html.haml | 7 + app/views/meetings/index.html.haml | 37 + app/views/meetings/index.json.jbuilder | 1 + app/views/meetings/new.html.haml | 5 + app/views/meetings/show.html.haml | 33 + app/views/meetings/show.json.jbuilder | 1 + app/views/simple_calendar/_calendar.html.erb | 33 + .../simple_calendar/_month_calendar.html.erb | 33 + .../simple_calendar/_week_calendar.html.erb | 39 + config/routes.rb | 4 + db/migrate/20230913160600_create_meetings.rb | 19 + db/schema.rb | 18 +- test/controllers/meetings_controller_test.rb | 51 + .../files/email_with_calendar_invite.eml | 893 ++++++++++++++++++ test/fixtures/meetings.yml | 23 + test/mailboxes/e_mailbox_test.rb | 11 + 27 files changed, 1510 insertions(+), 7 deletions(-) create mode 100644 app/controllers/comfy/admin/calendars_controller.rb create mode 100755 app/controllers/meetings_controller.rb create mode 100755 app/models/meeting.rb create mode 100644 app/views/comfy/admin/calendars/index.html.haml create mode 100755 app/views/meetings/_form.html.haml create mode 100755 app/views/meetings/_meeting.json.jbuilder create mode 100755 app/views/meetings/edit.html.haml create mode 100755 app/views/meetings/index.html.haml create mode 100755 app/views/meetings/index.json.jbuilder create mode 100755 app/views/meetings/new.html.haml create mode 100755 app/views/meetings/show.html.haml create mode 100755 app/views/meetings/show.json.jbuilder create mode 100755 app/views/simple_calendar/_calendar.html.erb create mode 100755 app/views/simple_calendar/_month_calendar.html.erb create mode 100755 app/views/simple_calendar/_week_calendar.html.erb create mode 100755 db/migrate/20230913160600_create_meetings.rb create mode 100644 test/controllers/meetings_controller_test.rb create mode 100644 test/fixtures/files/email_with_calendar_invite.eml create mode 100755 test/fixtures/meetings.yml diff --git a/Gemfile b/Gemfile index 4b6f874b2..c2cbb5f11 100644 --- a/Gemfile +++ b/Gemfile @@ -124,4 +124,7 @@ gem 'devise-two-factor', "4.0.2" gem "slowpoke" -gem "strong_migrations" \ No newline at end of file +gem "strong_migrations" +gem "simple_calendar", "~> 3.0" + +gem "icalendar", "~> 2.9" diff --git a/Gemfile.lock b/Gemfile.lock index 57e0415be..0b0451feb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -250,6 +250,9 @@ GEM multi_xml (>= 0.5.2) i18n (1.10.0) concurrent-ruby (~> 1.0) + icalendar (2.9.0) + ice_cube (~> 0.16) + ice_cube (0.16.4) image_processing (1.12.2) mini_magick (>= 4.9.5, < 5) ruby-vips (>= 2.0.17, < 3) @@ -417,6 +420,8 @@ GEM connection_pool (>= 2.2.2) rack (~> 2.0) redis (>= 4.2.0) + simple_calendar (3.0.2) + rails (>= 6.1) simplecov (0.21.2) docile (~> 1.1) simplecov-html (~> 0.11) @@ -535,6 +540,7 @@ DEPENDENCIES gravatar_image_tag groupdate httparty + icalendar (~> 2.9) image_processing (~> 1.12) jbuilder (~> 2.7) jsonapi-serializer @@ -559,6 +565,7 @@ DEPENDENCIES ros-apartment-sidekiq sass-rails (>= 6) selenium-webdriver + simple_calendar (~> 3.0) simple_discussion! simplecov sinatra diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss index dcd3b6d6d..b7b90278a 100644 --- a/app/assets/stylesheets/application.scss +++ b/app/assets/stylesheets/application.scss @@ -20,6 +20,7 @@ @import 'select2/dist/css/select2.css'; @import './direct_upload.scss'; @import 'daterangepicker/daterangepicker'; + @import "simple_calendar"; :root { --color-primary: #6C5BF5; diff --git a/app/controllers/comfy/admin/calendars_controller.rb b/app/controllers/comfy/admin/calendars_controller.rb new file mode 100644 index 000000000..ad2ff25fe --- /dev/null +++ b/app/controllers/comfy/admin/calendars_controller.rb @@ -0,0 +1,23 @@ +class Comfy::Admin::CalendarsController < Comfy::Admin::Cms::BaseController + layout "comfy/admin/cms" + before_action :check_email_authorization + def new + + end + + def index + # Scope your query to the dates being shown: + start_date = params.fetch(:start_date, Date.today).to_date + @meetings = Meeting.where(start_time: start_date.beginning_of_month.beginning_of_week..start_date.end_of_month.end_of_week) + end + + + private + + def check_email_authorization + unless current_user.can_manage_email + flash.alert = 'You do not have permission to manage email' + redirect_back(fallback_location: root_path) + end + end +end \ No newline at end of file diff --git a/app/controllers/meetings_controller.rb b/app/controllers/meetings_controller.rb new file mode 100755 index 000000000..86414e2aa --- /dev/null +++ b/app/controllers/meetings_controller.rb @@ -0,0 +1,142 @@ +class MeetingsController < Comfy::Admin::Cms::BaseController + before_action :set_meeting, only: %i[ show edit update destroy ] + before_action :check_email_authorization + + # GET /meetings or /meetings.json + def index + @meetings = Meeting.all + end + + # GET /meetings/1 or /meetings/1.json + def show + end + + # GET /meetings/new + def new + @meeting = Meeting.new + end + + # GET /meetings/1/edit + def edit + end + + # POST /meetings or /meetings.json + def create + @meeting = Meeting.new(meeting_params) + @meeting.external_meeting_id = "#{SecureRandom.uuid}.#{Apartment::Tenant.current}@#{ENV['APP_HOST']}" + @meeting.status = 'CONFIRMED' + @meeting.participant_emails = meeting_params[:participant_emails].filter{ |node| URI::MailTo::EMAIL_REGEXP.match?(node) } + + respond_to do |format| + if @meeting.save + # send .ics file to participants + cal = Icalendar::Calendar.new + filename = "Invitation: #{@meeting.name}" + from_address = "#{Apartment::Tenant.current}@#{ENV['APP_HOST']}" + # to generate outlook + if false == 'vcs' + cal.prodid = '-//Microsoft Corporation//Outlook MIMEDIR//EN' + cal.version = '1.0' + filename += '.vcs' + else # ical + cal.prodid = '-//Restarone Solutions, Inc.//NONSGML ExportToCalendar//EN' + cal.version = '2.0' + filename += '.ics' + end + cal.append_custom_property('METHOD', 'REQUEST') + cal.event do |e| + e.dtstart = Icalendar::Values::DateTime.new(@meeting.start_time, tzid: @meeting.timezone) + e.dtend = Icalendar::Values::DateTime.new(@meeting.end_time, tzid: @meeting.timezone) + e.organizer = Icalendar::Values::CalAddress.new("mailto:#{from_address}", cn: from_address) + e.attendee = Icalendar::Values::CalAddress.new("mailto:#{from_address}", partstat: 'ACCEPTED') + e.uid = @meeting.external_meeting_id + @meeting.participant_emails.each do |email| + attendee_params = { + "CUTYPE" => "INDIVIDUAL", + "ROLE" => "REQ-PARTICIPANT", + "PARTSTAT" => "NEEDS-ACTION", + "RSVP" => "TRUE", + } + + attendee_value = Icalendar::Values::Text.new("MAILTO:#{email}", attendee_params) + cal.append_custom_property("ATTENDEE", attendee_value) + end + e.description = @meeting.description + e.location = @meeting.location + e.sequence = Time.now.to_i + e.status = "CONFIRMED" + e.summary = @meeting.name + + + e.alarm do |a| + a.summary = "#{@meeting.name} starts in 30 minutes!" + a.trigger = '-PT30M' + end + end + file = cal.to_ical + attachment = { filename: filename, mime_type: "text/calendar;method=REQUEST;name=\'#{filename}\'", content: file } + blob = ActiveStorage::Blob.create_and_upload!(io: StringIO.new(attachment[:content]), filename: attachment[:filename], content_type: attachment[:mime_type], metadata: nil) + email_thread = MessageThread.create!(recipients: @meeting.participant_emails, subject: "Invitation: #{@meeting.name}") + email_content = <<-HTML +
+

You have been invited to the following meeting, please see details below

+

+ +
+ HTML + email_content += ActionText::Content.new("").to_s + email_message = email_thread.messages.create!( + content: email_content.html_safe, + from: from_address + ) + EMailer.with(message: email_message, message_thread: email_thread, attachments: [attachment]).ship.deliver_later + + format.html { redirect_to @meeting, notice: "Meeting was successfully created." } + format.json { render :show, status: :created, location: @meeting } + else + format.html { render :new, status: :unprocessable_entity } + format.json { render json: @meeting.errors, status: :unprocessable_entity } + end + end + end + + # PATCH/PUT /meetings/1 or /meetings/1.json + def update + respond_to do |format| + if @meeting.update(meeting_params) + format.html { redirect_to @meeting, notice: "Meeting was successfully updated." } + format.json { render :show, status: :ok, location: @meeting } + else + format.html { render :edit, status: :unprocessable_entity } + format.json { render json: @meeting.errors, status: :unprocessable_entity } + end + end + end + + # DELETE /meetings/1 or /meetings/1.json + def destroy + @meeting.destroy + respond_to do |format| + format.html { redirect_to meetings_url, notice: "Meeting was successfully destroyed." } + format.json { head :no_content } + end + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_meeting + @meeting = Meeting.find(params[:id]) + end + + # Only allow a list of trusted parameters through. + def meeting_params + params.require(:meeting).permit(:name, :start_time, :end_time, :description, :timezone, :location, participant_emails: []) + end + + def check_email_authorization + unless current_user.can_manage_email + flash.alert = 'You do not have permission to manage email' + redirect_back(fallback_location: root_path) + end + end +end diff --git a/app/mailboxes/e_mailbox.rb b/app/mailboxes/e_mailbox.rb index 4dadc97b8..64f2fa145 100755 --- a/app/mailboxes/e_mailbox.rb +++ b/app/mailboxes/e_mailbox.rb @@ -17,15 +17,17 @@ def process subject: subject ) end - + uploaded_attachments = attachments + multipart_attachments = multipart_attached message = Message.create!( email_message_id: mail.message_id, message_thread: message_thread, - content: body, + content: body(uploaded_attachments), from: mail.from.join(', '), - attachments: (attachments + multipart_attached).map{ |a| a[:blob] } + attachments: (uploaded_attachments + multipart_attachments).map{ |a| a[:blob] } ) message_thread.update(unread: true) + process_attachments(uploaded_attachments) if uploaded_attachments.size > 0 ApiNamespace::Plugin::V1::SubdomainEventsService.new(message).track_event end end @@ -60,10 +62,10 @@ def multipart_attached return blobs end - def body + def body(uploaded_blobs) if mail.multipart? && mail.html_part document = Nokogiri::HTML(mail.html_part.body.decoded) - attachments.map do |attachment_hash| + uploaded_blobs.map do |attachment_hash| attachment = attachment_hash[:original] blob = attachment_hash[:blob] if attachment.content_id.present? @@ -91,4 +93,35 @@ def sanitize_email_subject_prefixes(subject) # examples: 'Re: ', 're: ', 'FWD: ', 'Fwd: ', 'Fw: ' subject.gsub(/^((re|fw(d)?): )/i, '') end + + def process_attachments(blobs) + # process .ics files + ics_files = blobs.select {|attachment| attachment[:original].content_type.include?('.ics') } + ics_files.each do |ics_file| + ics_string = ics_file[:blob].download + calendars = Icalendar::Calendar.parse(ics_string) + calendars.each do |calendar| + calendar.events.each do |event| + existing_meeting = Meeting.find_by(external_meeting_id: event.uid.to_s) + if existing_meeting + # handle replies to meeting invites + existing_meeting.update(updated_at: Time.now) + else + meeting = Meeting.create!( + name: event.summary, + external_meeting_id: event.uid, + start_time: event.dtstart, + end_time: event.dtend, + timezone: Array.wrap(event.dtstart.ical_params['tzid']).join('-'), + description: event.description, + participant_emails: event.attendee.map{|uri| uri.to }, + location: event.location, + status: 'TENTATIVE', + custom_properties: event.custom_properties, + ) + end + end + end + end + end end diff --git a/app/models/meeting.rb b/app/models/meeting.rb new file mode 100755 index 000000000..3658923f7 --- /dev/null +++ b/app/models/meeting.rb @@ -0,0 +1,6 @@ +class Meeting < ApplicationRecord + STATUS_LIST = ['TENTATIVE', 'CONFIRMED', 'CANCELLED'] + # https://www.kanzaki.com/docs/ical/status.html + + validates :status, inclusion: { in: STATUS_LIST } +end diff --git a/app/views/comfy/admin/calendars/index.html.haml b/app/views/comfy/admin/calendars/index.html.haml new file mode 100644 index 000000000..cb7404271 --- /dev/null +++ b/app/views/comfy/admin/calendars/index.html.haml @@ -0,0 +1,38 @@ +.page-header + .h2 + Calendar + %p Early access, feature under active development + + += month_calendar(events: @meetings, attribute: :start_time) do |date, meetings| + = date + - meetings.each do |meeting| + %div + = meeting.name + +.h2 Listing meetings + +%table.table + %thead + %tr + %th Name + %th Start time + %th Participant emails + %th Status + %th + %th + + %tbody + - @meetings.each do |meeting| + %tr + %td + = link_to meeting.name, meeting + %td= meeting.start_time + %td= meeting.participant_emails + %td= meeting.status + %td= link_to 'Edit', edit_meeting_path(meeting) + %td= link_to 'Destroy', meeting, method: :delete, data: { confirm: 'Are you sure?' } + +%br + += link_to 'New Meeting', new_meeting_path \ No newline at end of file diff --git a/app/views/comfy/admin/cms/partials/_navigation_inner.haml b/app/views/comfy/admin/cms/partials/_navigation_inner.haml index 795f5775d..03c600e5b 100644 --- a/app/views/comfy/admin/cms/partials/_navigation_inner.haml +++ b/app/views/comfy/admin/cms/partials/_navigation_inner.haml @@ -1,6 +1,7 @@ %li{class: 'nav-item', data: { turbo: 'true'}} = active_link_to "Users", admin_users_path, class: 'nav-link' = active_link_to "Email", mailbox_path, class: 'nav-link' + = active_link_to "Calendar", calendars_path, class: 'nav-link' = active_link_to "API", api_namespaces_path, class: 'nav-link' = active_link_to "App Settings", edit_web_settings_path, class: 'nav-link' = active_link_to "Analytics", dashboard_path, class: 'nav-link' diff --git a/app/views/meetings/_form.html.haml b/app/views/meetings/_form.html.haml new file mode 100755 index 000000000..597fc13bb --- /dev/null +++ b/app/views/meetings/_form.html.haml @@ -0,0 +1,41 @@ += form_for @meeting do |f| + - if @meeting.errors.any? + #error_explanation + %h2= "#{pluralize(@meeting.errors.count, "error")} prohibited this meeting from being saved:" + %ul + - @meeting.errors.full_messages.each do |message| + %li= message + + .field + = f.label :name + = f.text_field :name + .field + = f.label :start_time + = f.datetime_select :start_time + .field + = f.label :end_time + = f.datetime_select :end_time + .field + = f.label :participant_emails + = f.select :participant_emails, options_for_select([]), { include_blank: false }, { multiple: true } + .field + = f.label :description + = f.text_area :description + .field + = f.label :timezone + = f.select :timezone, ActiveSupport::TimeZone::MAPPING + .field + = f.label :location + = f.text_field :location + .actions + = f.submit 'Save' + +:javascript + $(document).ready( function() { + $("#meeting_participant_emails").select2({ + multiple: true, + required: true, + tags: true, + tokenSeparators: [',', ' '], + }) + }); diff --git a/app/views/meetings/_meeting.json.jbuilder b/app/views/meetings/_meeting.json.jbuilder new file mode 100755 index 000000000..c6201e105 --- /dev/null +++ b/app/views/meetings/_meeting.json.jbuilder @@ -0,0 +1,2 @@ +json.extract! meeting, :id, :name, :start_time, :end_time, :participant_emails, :description, :timezone, :location, :status, :external_meeting_id, :created_at, :updated_at +json.url meeting_url(meeting, format: :json) diff --git a/app/views/meetings/edit.html.haml b/app/views/meetings/edit.html.haml new file mode 100755 index 000000000..6f30f1ca7 --- /dev/null +++ b/app/views/meetings/edit.html.haml @@ -0,0 +1,7 @@ +%h1 Editing meeting + += render 'form' + += link_to 'Show', @meeting +\| += link_to 'Back', meetings_path diff --git a/app/views/meetings/index.html.haml b/app/views/meetings/index.html.haml new file mode 100755 index 000000000..e53f203c4 --- /dev/null +++ b/app/views/meetings/index.html.haml @@ -0,0 +1,37 @@ +%h1 Listing meetings + +%table + %thead + %tr + %th Name + %th Start time + %th End time + %th Participant emails + %th Description + %th Timezone + %th Location + %th Status + %th External meeting + %th + %th + %th + + %tbody + - @meetings.each do |meeting| + %tr + %td= meeting.name + %td= meeting.start_time + %td= meeting.end_time + %td= meeting.participant_emails + %td= meeting.description + %td= meeting.timezone + %td= meeting.location + %td= meeting.status + %td= meeting.external_meeting_id + %td= link_to 'Show', meeting + %td= link_to 'Edit', edit_meeting_path(meeting) + %td= link_to 'Destroy', meeting, method: :delete, data: { confirm: 'Are you sure?' } + +%br + += link_to 'New Meeting', new_meeting_path diff --git a/app/views/meetings/index.json.jbuilder b/app/views/meetings/index.json.jbuilder new file mode 100755 index 000000000..bed849ce9 --- /dev/null +++ b/app/views/meetings/index.json.jbuilder @@ -0,0 +1 @@ +json.array! @meetings, partial: "meetings/meeting", as: :meeting diff --git a/app/views/meetings/new.html.haml b/app/views/meetings/new.html.haml new file mode 100755 index 000000000..92b9db5a8 --- /dev/null +++ b/app/views/meetings/new.html.haml @@ -0,0 +1,5 @@ +%h1 New meeting + += render 'form' + += link_to 'Back', meetings_path diff --git a/app/views/meetings/show.html.haml b/app/views/meetings/show.html.haml new file mode 100755 index 000000000..2aaae6345 --- /dev/null +++ b/app/views/meetings/show.html.haml @@ -0,0 +1,33 @@ +%p#notice= notice + +%p + %b Name: + = @meeting.name +%p + %b Start time: + = @meeting.start_time +%p + %b End time: + = @meeting.end_time +%p + %b Participant emails: + = @meeting.participant_emails +%p + %b Description: + = @meeting.description +%p + %b Timezone: + = @meeting.timezone +%p + %b Location: + = @meeting.location +%p + %b Status: + = @meeting.status +%p + %b External meeting: + = @meeting.external_meeting_id + += link_to 'Edit', edit_meeting_path(@meeting) +\| += link_to 'Back', meetings_path diff --git a/app/views/meetings/show.json.jbuilder b/app/views/meetings/show.json.jbuilder new file mode 100755 index 000000000..d8c2bae88 --- /dev/null +++ b/app/views/meetings/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! "meetings/meeting", meeting: @meeting diff --git a/app/views/simple_calendar/_calendar.html.erb b/app/views/simple_calendar/_calendar.html.erb new file mode 100755 index 000000000..898ad151b --- /dev/null +++ b/app/views/simple_calendar/_calendar.html.erb @@ -0,0 +1,33 @@ +
+
+ <%= t('date.month_names')[start_date.month] %> <%= start_date.year %> + + +
+ + + + + <% date_range.slice(0, 7).each do |day| %> + + <% end %> + + + + + <% date_range.each_slice(7) do |week| %> + <%= content_tag :tr, class: calendar.tr_classes_for(week) do %> + <% week.each do |day| %> + <%= content_tag :td, class: calendar.td_classes_for(day) do %> + <% instance_exec(day, calendar.sorted_events_for(day), &passed_block) %> + <% end %> + <% end %> + <% end %> + <% end %> + +
<%= t('date.abbr_day_names')[day.wday] %>
+
diff --git a/app/views/simple_calendar/_month_calendar.html.erb b/app/views/simple_calendar/_month_calendar.html.erb new file mode 100755 index 000000000..c47d71e3a --- /dev/null +++ b/app/views/simple_calendar/_month_calendar.html.erb @@ -0,0 +1,33 @@ +
+
+ + + +
+ + + + + <% date_range.slice(0, 7).each do |day| %> + + <% end %> + + + + + <% date_range.each_slice(7) do |week| %> + + <% week.each do |day| %> + <%= content_tag :td, class: calendar.td_classes_for(day) do %> + <% instance_exec(day, calendar.sorted_events_for(day), &passed_block) %> + <% end %> + <% end %> + + <% end %> + +
<%= t('date.abbr_day_names')[day.wday] %>
+
diff --git a/app/views/simple_calendar/_week_calendar.html.erb b/app/views/simple_calendar/_week_calendar.html.erb new file mode 100755 index 000000000..0320b505a --- /dev/null +++ b/app/views/simple_calendar/_week_calendar.html.erb @@ -0,0 +1,39 @@ +
+
+ + <%= t('simple_calendar.week', default: 'Week') %> + <%= calendar.week_number %> + <% if calendar.number_of_weeks > 1 %> + - <%= calendar.end_week %> + <% end %> + + + +
+ + + + + <% date_range.slice(0, 7).each do |day| %> + + <% end %> + + + + + <% date_range.each_slice(7) do |week| %> + + <% week.each do |day| %> + <%= content_tag :td, class: calendar.td_classes_for(day) do %> + <% instance_exec(day, calendar.sorted_events_for(day), &passed_block) %> + <% end %> + <% end %> + + <% end %> + +
<%= t('date.abbr_day_names')[day.wday] %>
+
diff --git a/config/routes.rb b/config/routes.rb index 35bfaa18a..7a4c73cb4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -46,6 +46,10 @@ def self.matches?(request) end end + # calendar / meetings functionality + resources :calendars, controller: 'comfy/admin/calendars' + resources :meetings + resource :web_settings, controller: 'comfy/admin/web_settings', only: [:edit, :update] resources :users, controller: 'comfy/admin/users', as: :admin_users, except: [:create, :show] do collection do diff --git a/db/migrate/20230913160600_create_meetings.rb b/db/migrate/20230913160600_create_meetings.rb new file mode 100755 index 000000000..576eabebf --- /dev/null +++ b/db/migrate/20230913160600_create_meetings.rb @@ -0,0 +1,19 @@ +class CreateMeetings < ActiveRecord::Migration[6.1] + def change + create_table :meetings do |t| + t.string :name + t.datetime :start_time, null: false + t.datetime :end_time, null: false + t.text :participant_emails, array: true, default: [] + t.text :description + t.string :timezone, null: false + t.string :location + t.string :status, null: false + t.string :external_meeting_id, null: false + t.jsonb :custom_properties, default: '{}' + + t.timestamps + end + add_index :meetings, :external_meeting_id, unique: true + end +end diff --git a/db/schema.rb b/db/schema.rb index 0fca04b3c..4c2e5e965 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2023_05_16_012035) do +ActiveRecord::Schema.define(version: 2023_09_13_160600) do # These are extensions that must be enabled in order to support this database enable_extension "plpgsql" @@ -429,6 +429,22 @@ t.datetime "updated_at", precision: 6, null: false end + create_table "meetings", force: :cascade do |t| + t.string "name" + t.datetime "start_time", null: false + t.datetime "end_time", null: false + t.text "participant_emails", default: [], array: true + t.text "description" + t.string "timezone", null: false + t.string "location" + t.string "status", null: false + t.string "external_meeting_id", null: false + t.jsonb "custom_properties", default: "{}" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["external_meeting_id"], name: "index_meetings_on_external_meeting_id", unique: true + end + create_table "message_threads", force: :cascade do |t| t.boolean "unread" t.datetime "deleted_at" diff --git a/test/controllers/meetings_controller_test.rb b/test/controllers/meetings_controller_test.rb new file mode 100644 index 000000000..db6cd9b9a --- /dev/null +++ b/test/controllers/meetings_controller_test.rb @@ -0,0 +1,51 @@ +require "test_helper" + +class MeetingsControllerTest < ActionDispatch::IntegrationTest + setup do + @meeting = meetings(:one) + @user = users(:public) + @user.update(can_manage_email: true) + sign_in(@user) + end + + test "should get index" do + get meetings_url + assert_response :success + end + + test "should get new" do + get new_meeting_url + assert_response :success + end + + test "should create meeting" do + assert_difference('Meeting.count') do + post meetings_url, params: { meeting: { description: @meeting.description, end_time: @meeting.end_time, location: @meeting.location, name: @meeting.name, participant_emails: ['contact@restarone.com'], start_time: @meeting.start_time, status: @meeting.status, timezone: @meeting.timezone } } + end + + assert_redirected_to meeting_url(Meeting.last) + end + + test "should show meeting" do + get meeting_url(@meeting) + assert_response :success + end + + test "should get edit" do + get edit_meeting_url(@meeting) + assert_response :success + end + + test "should update meeting" do + patch meeting_url(@meeting), params: { meeting: { description: @meeting.description, end_time: @meeting.end_time, location: @meeting.location, name: @meeting.name, participant_emails: @meeting.participant_emails, start_time: @meeting.start_time, status: "TENTATIVE", timezone: @meeting.timezone } } + assert_redirected_to meeting_url(@meeting) + end + + test "should destroy meeting" do + assert_difference('Meeting.count', -1) do + delete meeting_url(@meeting) + end + + assert_redirected_to meetings_url + end +end diff --git a/test/fixtures/files/email_with_calendar_invite.eml b/test/fixtures/files/email_with_calendar_invite.eml new file mode 100644 index 000000000..1d8fb341d --- /dev/null +++ b/test/fixtures/files/email_with_calendar_invite.eml @@ -0,0 +1,893 @@ +Delivered-To: restarone@restarone.solutions +Received: by 2002:a9a:7443:0:b0:26d:f5c7:8136 with SMTP id m3csp3319080lkn; + Wed, 13 Sep 2023 06:47:41 -0700 (PDT) +X-Received: by 2002:ad4:4446:0:b0:655:cf0f:6704 with SMTP id l6-20020ad44446000000b00655cf0f6704mr2657418qvt.5.1694612860430; + Wed, 13 Sep 2023 06:47:40 -0700 (PDT) +ARC-Seal: i=1; a=rsa-sha256; t=1694612860; cv=none; + d=google.com; s=arc-20160816; + b=yajeqbL7Up6JLXkzQUXVPUt0dHRL+EDPmJ/naSDyOxG+WYTeWrgoy7QCoghbwmUl99 + Y2FnsJoXXq7Igc/wxZhJz2u/BzDIhubb/0asmscO/98q9Dx6TeMNlBodtDyuMe99R711 + esEWka1GaBhRHokPl7gqNn9wjhpqmGqndEpNvfDZ0Pps/iYllEsdMzX5FtndCjddtr+Q + ANQlvSmXBD+VS2xU3zTM0nWJmpgFNDZhF1a+WgubwyaL6qbduzoPaBZ13Vvc5vhrlzPH + PNXnG+Q+/7a9VI6pqYXHux4rcrrkyCUq4dgE8POXss2IeASvULVjNvPIHg0ZcGwIYgCT + y31w== +ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816; + h=to:from:subject:date:message-id:sender:reply-to:mime-version + :dkim-signature:dkim-signature; + bh=jQjL48TRbWIpmFbKg7lGniGzhaf+TVo9+lJ67nhrZk4=; + fh=3YrH2AqvZR8DkO7aOt+6Qken8yFGbL96xDY6JnnMGNk=; + b=CGM8nMyhcs5B3t5Ha0O8sDCw0op+5YKXdxi3ooNSOcMPPFmg6KFQK5Hi+Cc4jDr8Y2 + M9N2mqq4DCGjbxxQTGY1rsB6E1MpepXAMUX+qV/W0wKi/4WX1GBaxcRJgbxfSW1Gk6Q7 + LDqcv7+lSAw1e+wP6syoPC/6z3xWq2NpcVUiscRHDiDzexn+nnwJE5BFutbWhzHip6yr + fWfDaskZMM/MUm5YG062ipqPWPaxsYD6KaJj8kfkcvRk6kPDIN6K78qc/Fr/wA2p6KR/ + AGxrcL8tDL2kdyHvNcoFUZLEY+mM8ICKTks8L+DoWCCW+tku79npMgGFZLtem/jOQGPe + KhTA== +ARC-Authentication-Results: i=1; mx.google.com; + dkim=pass header.i=@google.com header.s=20230601 header.b=4cb2kpAG; + dkim=pass header.i=@restarone.com header.s=google header.b=c9L85yhO; + spf=pass (google.com: domain of contact@restarone.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=contact@restarone.com +Return-Path: +Received: from mail-sor-f73.google.com (mail-sor-f73.google.com. [209.85.220.73]) + by mx.google.com with SMTPS id n6-20020a0c8c06000000b00631f654d349sor4820453qvb.5.2023.09.13.06.47.40 + for + (Google Transport Security); + Wed, 13 Sep 2023 06:47:40 -0700 (PDT) +Received-SPF: pass (google.com: domain of contact@restarone.com designates 209.85.220.73 as permitted sender) client-ip=209.85.220.73; +Authentication-Results: mx.google.com; + dkim=pass header.i=@google.com header.s=20230601 header.b=4cb2kpAG; + dkim=pass header.i=@restarone.com header.s=google header.b=c9L85yhO; + spf=pass (google.com: domain of contact@restarone.com designates 209.85.220.73 as permitted sender) smtp.mailfrom=contact@restarone.com +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=google.com; s=20230601; t=1694612860; x=1695217660; dara=google.com; + h=to:from:subject:date:message-id:sender:reply-to:mime-version:from + :to:cc:subject:date:message-id:reply-to; + bh=jQjL48TRbWIpmFbKg7lGniGzhaf+TVo9+lJ67nhrZk4=; + b=4cb2kpAGWYCCzwsVOEmFpeC5eH2DOe6TeebiP7ALlgSRToIQf9jZB4/9Z5TbLBn/Bz + /GLRwjF5RTuz9T1FKVNMokMUThUuHfQkAcyG+tkUBbqSqBDsl9rriAJJ7OzZaV1yFsH4 + p3yHakQ/oombYRGf9iVxrMrVpVPT27Ty4uE+CYjMCHIjg8l36iAHKGErJhVHpeVvDycn + RIOpaSeuDgaqWJ6cZRW5TnAuGpZf7xcgSFvJE7NU6StVTgIRaQ3h4RI0HmGwUuT6eHe/ + Gxvzh4Zyvr4jS8j0q/IaX65Mzs9y3ZAilTlcTjZhiw9ewnnlslprSXxRtAAShiXqN3DW + tKVQ== +DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=restarone.com; s=google; t=1694612860; x=1695217660; dara=google.com; + h=to:from:subject:date:message-id:sender:reply-to:mime-version:from + :to:cc:subject:date:message-id:reply-to; + bh=jQjL48TRbWIpmFbKg7lGniGzhaf+TVo9+lJ67nhrZk4=; + b=c9L85yhO9+lWc5YL4toKAMjgB6zObypswuclLnH0ZOl1IuODTHVuDwgZY9xlzOT4w3 + CvGGRjWe9Idks6oXDkSExHoXAA9qKrSAd8f6RW5rmeNDlTiHYMVaSNtEc1iTyKaHuA5l + xO4zy5OusYCEl5q5LSyWTN1I+tJJWINKVMhds= +X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=1e100.net; s=20230601; t=1694612860; x=1695217660; + h=to:from:subject:date:message-id:sender:reply-to:mime-version + :x-gm-message-state:from:to:cc:subject:date:message-id:reply-to; + bh=jQjL48TRbWIpmFbKg7lGniGzhaf+TVo9+lJ67nhrZk4=; + b=Sb+r+Abl1na8CpV0Ixdrmk7Bk/KUOT4arNsHMNG3GFKW+qMd0inEztuxkYVppq0GFq + i+KzwQ0HlpCWA83Thu8SLIigPPl1VsAI+nkNbX4yuG5las+/4JtlwIaDYRpIG4h+x1t+ + WGfcmE7QcbbHg5y803tuIqp1xgpnSZQ9JCOZECiJdHyiKhRrY3Uxyg9dotCk4xgFVv8h + YCA18S4qy/JsVHp3BWVqch8h6PHyhMojKCdMUzyxcChG2BnGrGcVabetzHGiqvUyERRx + vSq5KSF/c/D9KrWod/1Xs4pv7pYjQDE+ZU+a1gdZtmWZYUzDoJjWKqilbbaTZtMCEUqr + z84A== +X-Gm-Message-State: AOJu0YwNhqeENoTe+Fj9uDXO0JKv1g2PUZphLGjx3aOEW0t/yK1FSXCf + qJLe+otnSEuXxViGSI2/XL416S0SX3b5lARJR2RcFyHBaqUiaHM= +X-Google-Smtp-Source: AGHT+IEWeye6hY2V3qBxbAZLOMwHDvUCYFicSIxgxL92HWng5zyOUUxp9Ub9/xvyrUBr2ZwBi67P8XTuiQVHmUP3Y20U +MIME-Version: 1.0 +X-Received: by 2002:a05:6214:5002:b0:64f:946a:e2a6 with SMTP id + jo2-20020a056214500200b0064f946ae2a6mr3252403qvb.50.1694612859853; Wed, 13 + Sep 2023 06:47:39 -0700 (PDT) +Reply-To: contact@restarone.com +Sender: Google Calendar +Message-ID: +Date: Wed, 13 Sep 2023 13:47:39 +0000 +Subject: Invitation: test meeting @ Wed Sep 13, 2023 12:30pm - 1:30pm (EDT) (restarone@restarone.solutions) +From: contact@restarone.com +To: restarone@restarone.solutions +Content-Type: multipart/mixed; boundary="0000000000008b4dbe06053dcde8" + +--0000000000008b4dbe06053dcde8 +Content-Type: multipart/alternative; boundary="0000000000008b4dbd06053dcde6" + +--0000000000008b4dbd06053dcde6 +Content-Type: text/plain; charset="UTF-8"; format=flowed; delsp=yes +Content-Transfer-Encoding: base64 + +dGVzdCBtZWV0aW5nDQpXZWRuZXNkYXkgU2VwIDEzLCAyMDIzIOKLhSAxMjozMHBtIOKAkyAxOjMw +cG0NCkVhc3Rlcm4gVGltZSAtIFRvcm9udG8NCg0KSm9pbiB3aXRoIEdvb2dsZSBNZWV0DQpodHRw +czovL21lZXQuZ29vZ2xlLmNvbS9pb24tcmNkYy15cnE/aHM9MjI0DQoNCg0KCQ0KSm9pbiBieSBw +aG9uZQ0KKENBKSArMSAyODktMzQ4LTc3NzENClBJTjogNDIwMTQ0NjA3DQoNCk1vcmUgcGhvbmUg +bnVtYmVycw0KaHR0cHM6Ly90ZWwubWVldC9pb24tcmNkYy15cnE/cGluPTYxMjYxMjUyODcyMDUm +aHM9MA0KDQoNCk9yZ2FuaXplcg0KY29udGFjdEByZXN0YXJvbmUuY29tDQpjb250YWN0QHJlc3Rh +cm9uZS5jb20NCg0KUmVwbHkgZm9yIHNoYXNoaWtlamF5YXR1bmdlQGdtYWlsLmNvbSBhbmQgdmll +dyBtb3JlIGRldGFpbHMgIA0KaHR0cHM6Ly9jYWxlbmRhci5nb29nbGUuY29tL2NhbGVuZGFyL2V2 +ZW50P2FjdGlvbj1WSUVXJmVpZD1OM056WVRadE0yWXlOSEpqWjJoMWJUVTVabVppZGpkeGFEUWdj +MmhoYzJocGEyVnFZWGxoZEhWdVoyVkFiUSZ0b2s9TWpFalkyOXVkR0ZqZEVCeVpYTjBZWEp2Ym1V +dVkyOXRaVEEwT1RKa09UQTFaR1UyTlRCbE9XSXpNR1ppTW1Vd01qVmpOV05rTmpVeE1tUTNPV1Zo +WkEmY3R6PUFtZXJpY2ElMkZUb3JvbnRvJmhsPWVuJmVzPTENCllvdXIgYXR0ZW5kYW5jZSBpcyBv +cHRpb25hbC4NCg0Kfn4vL35+DQpJbnZpdGF0aW9uIGZyb20gR29vZ2xlIENhbGVuZGFyOiBodHRw +czovL2NhbGVuZGFyLmdvb2dsZS5jb20vY2FsZW5kYXIvDQoNCllvdSBhcmUgcmVjZWl2aW5nIHRo +aXMgZW1haWwgYmVjYXVzZSB5b3UgYXJlIGFuIGF0dGVuZGVlIG9uIHRoZSBldmVudC4gVG8gIA0K +c3RvcCByZWNlaXZpbmcgZnV0dXJlIHVwZGF0ZXMgZm9yIHRoaXMgZXZlbnQsIGRlY2xpbmUgdGhp +cyBldmVudC4NCg0KRm9yd2FyZGluZyB0aGlzIGludml0YXRpb24gY291bGQgYWxsb3cgYW55IHJl +Y2lwaWVudCB0byBzZW5kIGEgcmVzcG9uc2UgdG8gIA0KdGhlIG9yZ2FuaXplciwgYmUgYWRkZWQg +dG8gdGhlIGd1ZXN0IGxpc3QsIGludml0ZSBvdGhlcnMgcmVnYXJkbGVzcyBvZiAgDQp0aGVpciBv +d24gaW52aXRhdGlvbiBzdGF0dXMsIG9yIG1vZGlmeSB5b3VyIFJTVlAuDQoNCkxlYXJuIG1vcmUg +aHR0cHM6Ly9zdXBwb3J0Lmdvb2dsZS5jb20vY2FsZW5kYXIvYW5zd2VyLzM3MTM1I2ZvcndhcmRp +bmcNCg== +--0000000000008b4dbd06053dcde6 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + + + + + + + + + + + + =20 + + =20 + + =20 + + =20 + + + + + + + + + test meeting
Join with Google = +Meet =E2=80=93 You have been invited by contact@restarone.com to attend an = +event named test meeting on Wednesday Sep 13, 2023 =E2=8B=85 12:30pm =E2=80= +=93 1:30pm (Eastern Time - Toronto).
 
= +Join with Google Meet
= +
= +

Join by phone

(CA) +1 289-348-7771
PIN: 420144607<= +br>
More phone num= +bers
= +

When

Wednesday Sep 13, 202= +3 =E2=8B=85 12:30pm =E2=80=93 1:30pm (Eastern Time - Toronto)
<= +/td>

Organizer

<= +tr>

Guests

(Guest list has been h= +idden at organizer's request)
Yes
= +
<= +span itemprop=3D"potentialaction" itemscope itemtype=3D"http://schema.org/R= +svpAction">
No
<= +/table>
Maybe
More options
= +

Invitation from Google Calendar

You are receiving this email be= +cause you are subscribed to calendar notifications. To stop receiving these= + emails, go to Calendar settings<= +/a>, select this calendar, and change "Other notifications".

Forwardi= +ng this invitation could allow any recipient to send a response to the orga= +nizer, be added to the guest list, invite others regardless of their own in= +vitation status, or modify your RSVP. L= +earn more

= +
+--0000000000008b4dbd06053dcde6 +Content-Type: text/calendar; charset="UTF-8"; method=REQUEST +Content-Transfer-Encoding: 7bit + +BEGIN:VCALENDAR +PRODID:-//Google Inc//Google Calendar 70.9054//EN +VERSION:2.0 +CALSCALE:GREGORIAN +METHOD:REQUEST +BEGIN:VTIMEZONE +TZID:America/Toronto +X-LIC-LOCATION:America/Toronto +BEGIN:DAYLIGHT +TZOFFSETFROM:-0500 +TZOFFSETTO:-0400 +TZNAME:EDT +DTSTART:19700308T020000 +RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU +END:DAYLIGHT +BEGIN:STANDARD +TZOFFSETFROM:-0400 +TZOFFSETTO:-0500 +TZNAME:EST +DTSTART:19701101T020000 +RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU +END:STANDARD +END:VTIMEZONE +BEGIN:VEVENT +DTSTART;TZID=America/Toronto:20230913T123000 +DTEND;TZID=America/Toronto:20230913T133000 +DTSTAMP:20230913T134739Z +ORGANIZER;CN=contact@restarone.com:mailto:contact@restarone.com +UID:7ssa6m3f24rcghum59ffbv7qh4@google.com +ATTENDEE;CUTYPE=INDIVIDUAL;ROLE=REQ-PARTICIPANT;PARTSTAT=NEEDS-ACTION;RSVP= + TRUE;CN=restarone@restarone.solutions;X-NUM-GUESTS=0:mailto:restarone + @gmail.com +X-GOOGLE-CONFERENCE:https://meet.google.com/ion-rcdc-yrq +X-MICROSOFT-CDO-OWNERAPPTID:556337145 +CREATED:20230913T134737Z +DESCRIPTION:-::~:~::~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~ + :~:~:~:~:~:~:~:~::~:~::-\nJoin with Google Meet: https://meet.google.com/io + n-rcdc-yrq\nOr dial: (CA) +1 289-348-7771 PIN: 420144607#\nMore phone numbe + rs: https://tel.meet/ion-rcdc-yrq?pin=6126125287205&hs=7\n\nLearn more abou + t Meet at: https://support.google.com/a/users/answer/9282720\n\nPlease do n + ot edit this section.\n-::~:~::~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~:~: + ~:~:~:~:~:~:~:~:~:~:~:~:~:~::~:~::- +LAST-MODIFIED:20230913T134737Z +LOCATION: +SEQUENCE:0 +STATUS:CONFIRMED +SUMMARY:test meeting +TRANSP:OPAQUE +END:VEVENT +END:VCALENDAR + +--0000000000008b4dbd06053dcde6-- +--0000000000008b4dbe06053dcde8 +Content-Type: application/ics; name="invite.ics" +Content-Disposition: attachment; filename="invite.ics" +Content-Transfer-Encoding: base64 + +QkVHSU46VkNBTEVOREFSDQpQUk9ESUQ6LS8vR29vZ2xlIEluYy8vR29vZ2xlIENhbGVuZGFyIDcw +LjkwNTQvL0VODQpWRVJTSU9OOjIuMA0KQ0FMU0NBTEU6R1JFR09SSUFODQpNRVRIT0Q6UkVRVUVT +VA0KQkVHSU46VlRJTUVaT05FDQpUWklEOkFtZXJpY2EvVG9yb250bw0KWC1MSUMtTE9DQVRJT046 +QW1lcmljYS9Ub3JvbnRvDQpCRUdJTjpEQVlMSUdIVA0KVFpPRkZTRVRGUk9NOi0wNTAwDQpUWk9G +RlNFVFRPOi0wNDAwDQpUWk5BTUU6RURUDQpEVFNUQVJUOjE5NzAwMzA4VDAyMDAwMA0KUlJVTEU6 +RlJFUT1ZRUFSTFk7QllNT05USD0zO0JZREFZPTJTVQ0KRU5EOkRBWUxJR0hUDQpCRUdJTjpTVEFO +REFSRA0KVFpPRkZTRVRGUk9NOi0wNDAwDQpUWk9GRlNFVFRPOi0wNTAwDQpUWk5BTUU6RVNUDQpE +VFNUQVJUOjE5NzAxMTAxVDAyMDAwMA0KUlJVTEU6RlJFUT1ZRUFSTFk7QllNT05USD0xMTtCWURB +WT0xU1UNCkVORDpTVEFOREFSRA0KRU5EOlZUSU1FWk9ORQ0KQkVHSU46VkVWRU5UDQpEVFNUQVJU +O1RaSUQ9QW1lcmljYS9Ub3JvbnRvOjIwMjMwOTEzVDEyMzAwMA0KRFRFTkQ7VFpJRD1BbWVyaWNh +L1Rvcm9udG86MjAyMzA5MTNUMTMzMDAwDQpEVFNUQU1QOjIwMjMwOTEzVDEzNDczOVoNCk9SR0FO +SVpFUjtDTj1jb250YWN0QHJlc3Rhcm9uZS5jb206bWFpbHRvOmNvbnRhY3RAcmVzdGFyb25lLmNv +bQ0KVUlEOjdzc2E2bTNmMjRyY2dodW01OWZmYnY3cWg0QGdvb2dsZS5jb20NCkFUVEVOREVFO0NV +VFlQRT1JTkRJVklEVUFMO1JPTEU9UkVRLVBBUlRJQ0lQQU5UO1BBUlRTVEFUPU5FRURTLUFDVElP +TjtSU1ZQPQ0KIFRSVUU7Q049c2hhc2hpa2VqYXlhdHVuZ2VAZ21haWwuY29tO1gtTlVNLUdVRVNU +Uz0wOm1haWx0bzpzaGFzaGlrZWpheWF0dW5nZQ0KIEBnbWFpbC5jb20NClgtR09PR0xFLUNPTkZF +UkVOQ0U6aHR0cHM6Ly9tZWV0Lmdvb2dsZS5jb20vaW9uLXJjZGMteXJxDQpYLU1JQ1JPU09GVC1D +RE8tT1dORVJBUFBUSUQ6NTU2MzM3MTQ1DQpDUkVBVEVEOjIwMjMwOTEzVDEzNDczN1oNCkRFU0NS +SVBUSU9OOi06On46fjo6fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46 +fjp+On46fjp+On46fg0KIDp+On46fjp+On46fjp+On46On46fjo6LVxuSm9pbiB3aXRoIEdvb2ds +ZSBNZWV0OiBodHRwczovL21lZXQuZ29vZ2xlLmNvbS9pbw0KIG4tcmNkYy15cnFcbk9yIGRpYWw6 +IChDQSkgKzEgMjg5LTM0OC03NzcxIFBJTjogNDIwMTQ0NjA3I1xuTW9yZSBwaG9uZSBudW1iZQ0K +IHJzOiBodHRwczovL3RlbC5tZWV0L2lvbi1yY2RjLXlycT9waW49NjEyNjEyNTI4NzIwNSZocz03 +XG5cbkxlYXJuIG1vcmUgYWJvdQ0KIHQgTWVldCBhdDogaHR0cHM6Ly9zdXBwb3J0Lmdvb2dsZS5j +b20vYS91c2Vycy9hbnN3ZXIvOTI4MjcyMFxuXG5QbGVhc2UgZG8gbg0KIG90IGVkaXQgdGhpcyBz +ZWN0aW9uLlxuLTo6fjp+Ojp+On46fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46fjp+On46 +fjp+Og0KIH46fjp+On46fjp+On46fjp+On46fjp+On46fjo6fjp+OjotDQpMQVNULU1PRElGSUVE +OjIwMjMwOTEzVDEzNDczN1oNCkxPQ0FUSU9OOg0KU0VRVUVOQ0U6MA0KU1RBVFVTOkNPTkZJUk1F +RA0KU1VNTUFSWTp0ZXN0IG1lZXRpbmcNClRSQU5TUDpPUEFRVUUNCkVORDpWRVZFTlQNCkVORDpW +Q0FMRU5EQVINCg== +--0000000000008b4dbe06053dcde8-- diff --git a/test/fixtures/meetings.yml b/test/fixtures/meetings.yml new file mode 100755 index 000000000..89bf7d773 --- /dev/null +++ b/test/fixtures/meetings.yml @@ -0,0 +1,23 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + name: MyString + start_time: 2023-09-13 16:06:00 + end_time: 2023-09-13 16:06:00 + participant_emails: [] + description: MyText + timezone: MyString + location: MyString + status: CONFIRMED + external_meeting_id: MyString + +two: + name: MyString + start_time: 2023-09-13 16:06:00 + end_time: 2023-09-13 16:06:00 + participant_emails: [] + description: MyText + timezone: MyString + location: MyString + status: CONFIRMED + external_meeting_id: MyString2 diff --git a/test/mailboxes/e_mailbox_test.rb b/test/mailboxes/e_mailbox_test.rb index b0047ea8b..a47a67964 100755 --- a/test/mailboxes/e_mailbox_test.rb +++ b/test/mailboxes/e_mailbox_test.rb @@ -378,4 +378,15 @@ class EMailboxTest < ActionMailbox::TestCase test "new message in thread sets thread unread: true" do # todo test https://github.com/restarone/violet_rails/blob/57739a34ea8927ba222a42d372908a82e35de8cf/app/mailboxes/e_mailbox.rb end + + test "when sent with calendar invite" do + Apartment::Tenant.switch @restarone_subdomain do + assert_difference "Message.all.reload.size", +1 do + assert_difference "Meeting.all.reload.size", +1 do + email = create_inbound_email_from_fixture('email_with_calendar_invite.eml') + email.tap(&:route) + end + end + end + end end