Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ def duplicate_in_next_meeting_action_item(menu)
def next_meeting_action_item(menu, label:, action:, icon:)
return unless has_next_occurrence?

from_time = @meeting.start_time.past? ? Time.current : @meeting.scheduled_meeting.start_time
from_time = @meeting.start_time.past? ? Time.current : (@meeting.recurrence_start_time || @meeting.start_time)
result = @series.first_non_cancelled_occurrence(from_time:)
return if result.nil?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ See COPYRIGHT and LICENSE files for more details.
action: destroy_scheduled_project_recurring_meeting_path(
@project,
@series,
start_time: @scheduled_meeting.start_time.iso8601
start_time: @scheduled_meeting.recurrence_start_time.iso8601
),
method: :delete,
data: { turbo: true }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ class DeleteScheduledDialogComponent < ApplicationComponent
include ApplicationHelper
include OpTurbo::Streamable

def initialize(scheduled_meeting:)
def initialize(meeting_to_cancel:)
super

@scheduled_meeting = scheduled_meeting
@series = scheduled_meeting.recurring_meeting
@scheduled_meeting = meeting_to_cancel
@series = meeting_to_cancel.recurring_meeting
@project = @series.project
end

Expand Down
46 changes: 27 additions & 19 deletions modules/meeting/app/components/recurring_meetings/row_component.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,20 @@

module RecurringMeetings
class RowComponent < ::OpPrimer::BorderBoxRowComponent
delegate :meeting, to: :model
delegate :cancelled?, to: :model
delegate :recurring_meeting, to: :model
delegate :recurring_meeting, :cancelled?, to: :model
delegate :project, to: :recurring_meeting
delegate :schedule, to: :meeting

def meeting
model.is_a?(Meeting) ? model : nil
end

def instantiated?
meeting.present?
meeting.present? && !cancelled?
end

# The canonical scheduled time for this occurrence slot.
def occurrence_time
model.recurrence_start_time
end

def start_time
Expand All @@ -57,25 +63,25 @@ def formatted_time(time)
end

def old_time
render(Primer::Beta::Text.new(tag: :s)) { formatted_time(model.start_time) }
render(Primer::Beta::Text.new(tag: :s)) { formatted_time(occurrence_time) }
end

def start_time_title
if start_time_changed?
old_time + simple_format("\n#{formatted_time(meeting.start_time)}")
else
formatted_time(model.start_time)
formatted_time(occurrence_time)
end
end

def relative_time
time = start_time_changed? ? meeting.start_time : model.start_time
time = start_time_changed? ? meeting.start_time : occurrence_time

render(OpPrimer::RelativeTimeComponent.new(datetime: user_time_zone(time), prefix: I18n.t(:label_on)))
end

def state
if model.cancelled?
if cancelled?
"cancelled"
elsif instantiated?
meeting.state
Expand Down Expand Up @@ -112,7 +118,7 @@ def create
size: :medium,
tag: :a,
data: { "turbo-method": "post" },
href: init_project_recurring_meeting_path(project, model.recurring_meeting.id, start_time: model.start_time.iso8601)
href: init_project_recurring_meeting_path(project, recurring_meeting.id, start_time: occurrence_time.iso8601)
)
) do |_c|
I18n.t(:label_recurring_meeting_create)
Expand Down Expand Up @@ -150,8 +156,8 @@ def open_action(menu)
tag: :a,
href: init_project_recurring_meeting_path(
project,
model.recurring_meeting.id,
start_time: model.start_time.iso8601
recurring_meeting.id,
start_time: occurrence_time.iso8601
),
content_arguments: {
data: { turbo_method: :post }
Expand All @@ -166,11 +172,11 @@ def creatable?
end

def ical_action(menu)
return unless instantiated? && !cancelled?
return unless instantiated?

menu.with_item(label: I18n.t(:label_icalendar_download),
href: download_ics_project_recurring_meeting_path(project,
model.recurring_meeting,
recurring_meeting,
occurrence_id: meeting.id),
content_arguments: {
data: { turbo: false }
Expand Down Expand Up @@ -202,8 +208,8 @@ def delete_scheduled_action(menu)
label: I18n.t(:label_recurring_meeting_cancel),
scheme: :danger,
href: delete_scheduled_dialog_project_recurring_meeting_path(project,
model.recurring_meeting,
start_time: model.start_time.iso8601),
recurring_meeting,
start_time: occurrence_time.iso8601),
tag: :a,
content_arguments: {
data: { controller: "async-dialog" }
Expand All @@ -218,7 +224,7 @@ def restore_action(menu)

menu.with_item(
label: I18n.t(:label_recurring_meeting_restore),
href: init_project_recurring_meeting_path(project, recurring_meeting, start_time: model.start_time.iso8601),
href: init_project_recurring_meeting_path(project, recurring_meeting, start_time: occurrence_time.iso8601),
form_arguments: {
method: :post
}
Expand All @@ -235,12 +241,14 @@ def copy_allowed?
User.current.allowed_in_project?(:create_meetings, project)
end

# A non-cancelled meeting whose actual start_time differs from its canonical
# recurrence_start_time slot has been moved to a different time.
def start_time_changed?
meeting && meeting.start_time != model.start_time
instantiated? && meeting.start_time != occurrence_time
end

def past?
model.start_time < Time.current
occurrence_time < Time.current
end
end
end
3 changes: 3 additions & 0 deletions modules/meeting/app/contracts/meetings/base_contract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ def self.model
validate_sharing_only_on_onetime_templates
end

attribute :recurrence_start_time,
writable: ->(*) { model.recurring? }

private

def validate_sharing_only_on_onetime_templates
Expand Down
6 changes: 3 additions & 3 deletions modules/meeting/app/contracts/meetings/update_contract.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ def valid_rescheduling_date # rubocop:disable Metrics/AbcSize
return
end

check_before(model.scheduled_meeting.next_occurrence)
check_after(model.scheduled_meeting.previous_occurrence)
check_before(model.recurring_meeting.next_occurrence(from_time: model.recurrence_start_time))
check_after(model.recurring_meeting.previous_occurrence(from_time: model.recurrence_start_time))
check_after(model.recurring_meeting.first_occurrence)
end

Expand All @@ -78,7 +78,7 @@ def check_after(time)

def check_reschedule?
model.recurring_meeting_id &&
model.scheduled_meeting &&
model.recurrence_start_time.present? &&
model.changed.intersect?(%w[start_time start_date start_time_hour])
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ def check_recurring_meeting_param
end

def find_existing_occurrence
next_occurrence = @series.scheduled_meetings.find_by(start_time: @next_meeting_time)
next_occurrence = @series.meetings.not_templated.find_by(recurrence_start_time: @next_meeting_time)

if next_occurrence&.cancelled?
result = @series.first_non_cancelled_occurrence(from_time: @next_meeting_time)
Expand All @@ -402,10 +402,10 @@ def find_existing_occurrence
end

@next_meeting_time = result[:occurrence]
next_occurrence = @series.scheduled_meetings.find_by(start_time: @next_meeting_time)
next_occurrence = @series.meetings.not_templated.find_by(recurrence_start_time: @next_meeting_time)
end

@next_occurrence = next_occurrence&.meeting
@next_occurrence = next_occurrence&.cancelled? ? nil : next_occurrence
end

def render_next_meeting_flash(base_key, next_occurrence)
Expand Down
6 changes: 1 addition & 5 deletions modules/meeting/app/controllers/meetings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,7 @@ def show
format.pdf { export_pdf }
format.html do
html_title "#{t(:label_meeting)}: #{@meeting.title}"
if @meeting.state == "cancelled"
render_404
else
render(Meetings::ShowComponent.new(meeting: @meeting, state: show_edit_state), layout: true)
end
render(Meetings::ShowComponent.new(meeting: @meeting, state: show_edit_state), layout: true)
end
end
end
Expand Down
73 changes: 51 additions & 22 deletions modules/meeting/app/controllers/recurring_meetings_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class RecurringMeetingsController < ApplicationController
before_action :load_and_authorize_in_optional_project
before_action :find_recurring_meeting, except: %i[index new create]

before_action :get_scheduled_meeting, only: %i[delete_scheduled_dialog destroy_scheduled]
before_action :get_meeting_to_cancel, only: %i[delete_scheduled_dialog destroy_scheduled]
before_action :redirect_to_project, only: %i[show]
before_action :set_direction, only: %i[show]
before_action :convert_params, only: %i[create update]
Expand Down Expand Up @@ -51,12 +51,13 @@ def new
end

def init # rubocop:disable Metrics/AbcSize
scheduled_meeting = @recurring_meeting.scheduled_meetings.find_by(start_time: params[:start_time])
is_restoration = scheduled_meeting&.cancelled?
start_time = DateTime.iso8601(params[:start_time])
existing = @recurring_meeting.meetings.not_templated.find_by(recurrence_start_time: start_time)
is_restoration = existing&.cancelled?

call = ::RecurringMeetings::InitOccurrenceService
.new(user: current_user, recurring_meeting: @recurring_meeting)
.call(start_time: DateTime.iso8601(params[:start_time]))
.call(start_time:)

if call.success?
send_restoration_notifications(call.result) if is_restoration
Expand Down Expand Up @@ -190,12 +191,15 @@ def template_completed # rubocop:disable Metrics/AbcSize

def delete_scheduled_dialog
respond_with_dialog RecurringMeetings::DeleteScheduledDialogComponent.new(
scheduled_meeting: @scheduled_meeting
meeting_to_cancel: @meeting_to_cancel
)
end

def destroy_scheduled
if @scheduled_meeting.update(cancelled: true)
def destroy_scheduled # rubocop:disable Metrics/AbcSize
if @meeting_to_cancel.persisted?
meeting.update_column(:state, Meeting.states[:cancelled])
flash[:notice] = I18n.t(:notice_successful_cancel)
elsif @meeting_to_cancel.save
flash[:notice] = I18n.t(:notice_successful_cancel)
else
flash[:error] = I18n.t(:error_failed_to_delete_entry)
Expand Down Expand Up @@ -285,11 +289,11 @@ def send_restoration_notifications(meeting)
def upcoming_meetings(count:) # rubocop:disable Metrics/AbcSize
opened = @recurring_meeting
.upcoming_instantiated_meetings
.index_by(&:start_time)
.index_by(&:recurrence_start_time)

cancelled = @recurring_meeting
.upcoming_cancelled_meetings
.index_by(&:start_time)
.index_by(&:recurrence_start_time)

# Planned meetings consist of scheduled occurrences and cancelled meetings
# Open meetings are removed from the scheduled occurrences as they are displayed separately
Expand All @@ -300,15 +304,15 @@ def upcoming_meetings(count:) # rubocop:disable Metrics/AbcSize
# Get +1 scheduled_occurrences in case there is an ongoing cancelled occurrence
scheduled_times = @recurring_meeting
.scheduled_occurrences(limit: count + 1, from_time:)
.reject { |start_time| opened.include?(start_time) }
.reject { |occurrence_time| opened.include?(occurrence_time) }

has_ongoing = scheduled_times.any? { |start_time| start_time < Time.current }
has_ongoing = scheduled_times.any? { |occurrence_time| occurrence_time < Time.current }

planned = scheduled_times
.map { |start_time| cancelled[start_time] || scheduled_meeting(start_time) }
.map { |occurrence_time| cancelled[occurrence_time] || planned_occurrence(occurrence_time) }
.first([(count + (has_ongoing ? 1 : 0)), 0].max)

[opened.values.sort_by(&:start_time), planned]
[opened.values.sort_by(&:recurrence_start_time), planned]
end

def set_direction
Expand All @@ -329,14 +333,39 @@ def build_meeting_limits # rubocop:disable Metrics/AbcSize
@count = [show_more_limit_param(limit: params[:limit]), @max_count].compact.min
end

def scheduled_meeting(start_time)
ScheduledMeeting.new(start_time:, recurring_meeting: @recurring_meeting)
def planned_occurrence(recurrence_start_time)
RecurringMeetings::PlannedOccurrence.new(recurrence_start_time:, recurring_meeting: @recurring_meeting)
end

def get_scheduled_meeting
@scheduled_meeting = @recurring_meeting.scheduled_meetings.find_or_initialize_by(start_time: params[:start_time])
# Builds a Meeting object for a planned-but-not-yet-instantiated occurrence that
# the user wants to cancel. Returns 400 if an instantiated (non-cancelled) meeting
# already exists for this slot.
def get_meeting_to_cancel
recurrence_start_time = DateTime.iso8601(params[:start_time])
existing = @recurring_meeting.meetings.not_templated.find_by(recurrence_start_time:)

render_400 unless @scheduled_meeting.meeting_id.nil?
if existing && !existing.cancelled?
render_400
return
end

@meeting_to_cancel = existing || build_cancelled_occurrence(recurrence_start_time)
end

def build_cancelled_occurrence(recurrence_start_time)
template = @recurring_meeting.template
Meeting.new(
title: template.title,
project: @recurring_meeting.project,
author: current_user,
recurring_meeting: @recurring_meeting,
duration: template.duration,
location: template.location,
start_time: recurrence_start_time,
recurrence_start_time:,
state: :cancelled,
template: false
)
end

def visible_recurring_meetings_scope
Expand Down Expand Up @@ -388,10 +417,10 @@ def check_template_completable
end

is_scheduled = @recurring_meeting
.scheduled_meetings
.where(start_time: @first_occurrence)
.where.not(meeting_id: nil)
.exists?
.meetings
.not_templated
.not_cancelled
.exists?(recurrence_start_time: @first_occurrence)

if is_scheduled
flash[:info] = I18n.t("recurring_meeting.occurrence.first_already_exists")
Expand Down
Loading
Loading