Skip to content

Commit

Permalink
Merge pull request #14930 from opf/fix/meeting-attachments-copy
Browse files Browse the repository at this point in the history
Copy meetings with attachments in separate service
  • Loading branch information
klaustopher committed Mar 6, 2024
2 parents 327ee13 + ad8fc8b commit 56ed50b
Show file tree
Hide file tree
Showing 13 changed files with 499 additions and 107 deletions.
1 change: 1 addition & 0 deletions modules/meeting/app/contracts/meetings/base_contract.rb
Expand Up @@ -39,6 +39,7 @@ def self.model
attribute :duration
attribute :state
attribute :start_date
attribute :start_time
attribute :start_time_hour
end
end
49 changes: 49 additions & 0 deletions modules/meeting/app/contracts/meetings/create_contract.rb
@@ -0,0 +1,49 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Meetings
class CreateContract < BaseContract
attribute :type
validate :user_allowed_to_add
validate :type_in_allowed

private

def type_in_allowed
unless [StructuredMeeting.name, Meeting.name].include?(model.type)
errors.add(:type, :inclusion)
end
end

def user_allowed_to_add
unless user.allowed_in_project?(:create_meetings, model.project)
errors.add :base, :error_unauthorized
end
end
end
end
57 changes: 22 additions & 35 deletions modules/meeting/app/controllers/meetings_controller.rb
Expand Up @@ -29,7 +29,7 @@
class MeetingsController < ApplicationController
around_action :set_time_zone
before_action :find_optional_project, only: %i[index new create]
before_action :build_meeting, only: %i[new create]
before_action :build_meeting, only: %i[new]
before_action :find_meeting, except: %i[index new create]
before_action :find_copy_from_meeting, only: %i[create]
before_action :convert_params, only: %i[create update update_participants]
Expand Down Expand Up @@ -71,21 +71,28 @@ def show
end

def create
@meeting.participants.clear # Start with a clean set of participants
@meeting.participants_attributes = @converted_params.delete(:participants_attributes)
@meeting.attributes = @converted_params
copy_meeting_agenda
call =
if @copy_from
::Meetings::CopyService
.new(user: current_user, model: @copy_from)
.call(attributes: @converted_params, copy_agenda: params[:copy_agenda] == '1')
else
::Meetings::CreateService
.new(user: current_user)
.call(@converted_params)
end

if @meeting.save
if call.success?
text = I18n.t(:notice_successful_create)
if User.current.time_zone.nil?
link = I18n.t(:notice_timezone_missing, zone: Time.zone)
text += " #{view_context.link_to(link, { controller: '/my', action: :account }, class: 'link_to_profile')}"
end
flash[:notice] = text.html_safe

redirect_to action: 'show', id: @meeting
redirect_to action: 'show', id: call.result
else
@meeting = call.result
render template: 'meetings/new', project_id: @project, locals: { copy_from: @copy_from }
end
end
Expand All @@ -98,7 +105,11 @@ def new; end

def copy
copy_from = @meeting
@meeting = @meeting.copy(author: User.current)
call = ::Meetings::CopyService
.new(user: current_user, model: copy_from)
.call(save: false)

@meeting = call.result
render action: 'new', project_id: @project, locals: { copy_from: }
end

Expand Down Expand Up @@ -269,8 +280,7 @@ def set_time_zone(&)
end

def build_meeting
cls = meeting_type(params.dig(:meeting, :type)).constantize
@meeting = cls.new
@meeting = Meeting.new
@meeting.project = @project
@meeting.author = User.current
end
Expand All @@ -295,6 +305,7 @@ def convert_params
# instance variable.
@converted_params = meeting_params.to_h

@converted_params[:project] = @project
@converted_params[:duration] = @converted_params[:duration].to_hours if @converted_params[:duration].present?
# Force defaults on participants
@converted_params[:participants_attributes] ||= {}
Expand All @@ -304,7 +315,7 @@ def convert_params
def meeting_params
if params[:meeting].present?
params.require(:meeting).permit(:title, :location, :start_time,
:duration, :start_date, :start_time_hour,
:duration, :start_date, :start_time_hour, :type,
participants_attributes: %i[email name invited attended user user_id meeting id])
end
end
Expand All @@ -317,35 +328,11 @@ def structured_meeting_params
end
end

def meeting_type(given_type)
case given_type
when 'dynamic'
'StructuredMeeting'
else
'Meeting'
end
end

def find_copy_from_meeting
return unless params[:copied_from_meeting_id]

@copy_from = Meeting.visible.find(params[:copied_from_meeting_id])
rescue ActiveRecord::RecordNotFound
render_404
end

def copy_meeting_agenda
return unless params[:copy_agenda] == '1' && @copy_from

if @meeting.is_a?(StructuredMeeting)
@meeting.agenda_items_attributes = @copy_from.agenda_items.map(&:copy_attributes)
else
@meeting.agenda = MeetingAgenda.new(
author: current_user,
text: @copy_from.agenda&.text,
journal_notes: I18n.t('meeting.copied', id: params[:copied_from_meeting_id])
)
@meeting.agenda.author = current_user
end
end
end
29 changes: 7 additions & 22 deletions modules/meeting/app/models/meeting.rb
Expand Up @@ -121,6 +121,7 @@ def start_time

def start_time=(value)
super(value&.to_datetime)
update_derived_fields
end

def start_month
Expand Down Expand Up @@ -173,25 +174,6 @@ def all_changeable_participants
.uniq(&:id)
end

def copy(attrs)
copy = dup

# Set a default to next week
copy.start_time = start_time + 1.week

copy.author = attrs.delete(:author)
copy.attributes = attrs
copy.set_initial_values
# Initialize virtual attributes
copy.start_date
copy.start_time_hour

copy.participants.clear
copy.participants_attributes = allowed_participants.collect(&:copy_attributes)

copy
end

def self.group_by_time(meetings)
by_start_year_month_date = ActiveSupport::OrderedHash.new do |hy, year|
hy[year] = ActiveSupport::OrderedHash.new do |hm, month|
Expand Down Expand Up @@ -236,13 +218,11 @@ def close_agenda_and_copy_to_minutes!

def participants_attributes=(attrs)
attrs.each do |participant|
participant['_destroy'] = true if !(participant['attended'] || participant['invited'])
participant['_destroy'] = true if !(participant[:attended] || participant[:invited])
end
self.original_participants_attributes = attrs
end

protected

# Participants of older meetings
# might contain users no longer in the project
#
Expand All @@ -254,11 +234,16 @@ def allowed_participants
.where(user_id: available_members)
end

protected

def set_initial_values
# set defaults
write_attribute(:start_time, Date.tomorrow + 10.hours) if start_time.nil?
self.duration ||= 1
update_derived_fields
end

def update_derived_fields
@start_date = start_time.to_date.iso8601
@start_time_hour = start_time.strftime('%H:%M')
end
Expand Down
118 changes: 118 additions & 0 deletions modules/meeting/app/services/meetings/copy_service.rb
@@ -0,0 +1,118 @@
#-- copyright
# OpenProject is an open source project management software.
# Copyright (C) 2012-2024 the OpenProject GmbH
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License version 3.
#
# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows:
# Copyright (C) 2006-2013 Jean-Philippe Lang
# Copyright (C) 2010-2013 the ChiliProject Team
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# See COPYRIGHT and LICENSE files for more details.
#++

module Meetings
class CopyService
include ::Shared::ServiceContext
include ::Contracted
include ::Copy::Concerns::CopyAttachments

attr_accessor :user,
:meeting,
:contract_class

def initialize(user:, model:, contract_class: Meetings::CreateContract)
self.user = user
self.meeting = model
self.contract_class = contract_class
end

def call(send_notifications: nil, save: true, copy_agenda: true, copy_attachments: true, attributes: {})
if save
create(meeting, attributes, send_notifications:, copy_agenda:, copy_attachments:)
else
build(meeting, attributes)
end
end

protected

def create(meeting, attribute_overrides, send_notifications:, copy_agenda:, copy_attachments:)
Meetings::CreateService
.new(user:, contract_class:)
.call(**copied_attributes(meeting, attribute_overrides).merge(send_notifications:).symbolize_keys)
.on_success do |call|
copy_meeting_agenda(call.result) if copy_agenda
copy_meeting_attachment(call.result) if copy_attachments
end
end

def build(meeting, attribute_overrides)
Meetings::SetAttributesService
.new(user:, model: meeting.dup, contract_class:)
.call(**copied_attributes(meeting, attribute_overrides).symbolize_keys)
end

def copied_attributes(meeting, override)
overwritten_attributes = override.stringify_keys

meeting
.attributes
.slice(*writable_meeting_attributes(meeting))
.merge('start_time' => meeting.start_time + 1.week)
.merge('author' => user)
.merge('participants_attributes' => meeting.allowed_participants.collect(&:copy_attributes))
.merge(overwritten_attributes)
end

def writable_meeting_attributes(meeting)
instantiate_contract(meeting, user).writable_attributes - %w[start_date start_time_hour]
end

def copy_meeting_attachment(copy)
copy_attachments(
'Meeting',
from: meeting,
to: copy
)
end

def update_references(attachment_source:, attachment_target:, model_source:, model_target:, references:)
model_target
.agenda_items
.update_all(["notes = REPLACE(notes, '/attachments/?/', '/attachments/?/')",
attachment_source,
attachment_target])
end

def copy_meeting_agenda(copy)
if meeting.is_a?(StructuredMeeting)
meeting.agenda_items.each do |agenda_item|
copy.agenda_items << agenda_item.dup
end
else
MeetingAgenda.create!(
meeting: copy,
author: user,
text: meeting.agenda&.text,
journal_notes: I18n.t('meeting.copied', id: meeting.id)
)
end
end
end
end

0 comments on commit 56ed50b

Please sign in to comment.