canvas-lms/app/models/calendar_event.rb

672 lines
25 KiB
Ruby
Raw Normal View History

2011-02-01 09:57:29 +08:00
#
# Copyright (C) 2011 - 2014 Instructure, Inc.
2011-02-01 09:57:29 +08:00
#
# This file is part of Canvas.
#
# Canvas is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, version 3 of the License.
#
# Canvas 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 Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License along
# with this program. If not, see <http://www.gnu.org/licenses/>.
#
require 'date'
class CalendarEvent < ActiveRecord::Base
include CopyAuthorizedLinks
include TextHelper
include HtmlTextHelper
attr_accessible :title, :description, :start_at, :end_at, :location_name,
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
:location_address, :time_zone_edited, :cancel_reason,
:participants_per_appointment, :child_event_data,
:remove_child_events, :all_day
attr_accessor :cancel_reason, :imported
sanitize_field :description, CanvasSanitize::SANITIZE
copy_authorized_links(:description) { [self.effective_context, nil] }
2011-02-01 09:57:29 +08:00
include Workflow
2011-02-01 09:57:29 +08:00
belongs_to :context, :polymorphic => true
belongs_to :user
belongs_to :parent_event, :class_name => 'CalendarEvent', :foreign_key => :parent_calendar_event_id
has_many :child_events, :class_name => 'CalendarEvent', :foreign_key => :parent_calendar_event_id, :conditions => "calendar_events.workflow_state <> 'deleted'"
validates_presence_of :context, :workflow_state
validates_associated :context, :if => lambda { |record| record.validate_context }
2011-02-01 09:57:29 +08:00
validates_length_of :description, :maximum => maximum_long_text_length, :allow_nil => true, :allow_blank => true
validates_length_of :title, :maximum => maximum_string_length, :allow_nil => true, :allow_blank => true
2011-02-01 09:57:29 +08:00
before_save :default_values
after_save :touch_context
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
after_save :replace_child_events
after_save :sync_parent_event
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
after_update :sync_child_events
# when creating/updating a calendar_event, you can give it a list of child
# events. these will update/replace any existing child events. the format is:
# [{:start_at => start_at, :end_at => end_at, :context_code => context_code},
# {:start_at => start_at, :end_at => end_at, :context_code => context_code},
# ...]
# the context for each child event must have this event's context as its
# parent_event_context, and there can only be one event per context.
# remove_child_events can be set to remove all existing events (since rails
# form handling mechanism doesn't let you specify an empty array)
attr_accessor :child_event_data, :remove_child_events, :child_event_contexts
validates_each :child_event_data do |record, attr, events|
next unless events || Canvas::Plugin.value_to_boolean(record.remove_child_events)
events ||= []
events = events.values if events.is_a?(Hash)
next record.errors.add(attr, t('errors.no_updating_user', "Can't update child events unless an updating_user is set")) if events.present? && !record.updating_user
context_codes = events.map{ |e| e[:context_code] }
next record.errors.add(attr, t('errors.duplicate_child_event_contexts', "Duplicate child event contexts")) if context_codes != context_codes.uniq
contexts = find_all_by_asset_string(context_codes).group_by(&:asset_string)
context_codes.each do |code|
context = contexts[code] && contexts[code][0]
next if context && context.grants_right?(record.updating_user, :manage_calendar) && context.try(:parent_event_context) == record.context
break record.errors.add(attr, t('errors.invalid_child_event_context', "Invalid child event context"))
end
record.child_event_contexts = contexts
record.child_event_data = events
end
def replace_child_events
return unless @child_event_data
current_events = child_events.group_by{ |e| e[:context_code] }
@child_event_data.each do |data|
if event = current_events.delete(data[:context_code]) and event = event[0]
event.updating_user = @updating_user
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
event.update_attributes(:start_at => data[:start_at], :end_at => data[:end_at])
else
context = @child_event_contexts[data[:context_code]][0]
event = child_events.build(:start_at => data[:start_at], :end_at => data[:end_at])
event.updating_user = @updating_user
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
event.context = context
event.skip_sync_parent_event = true
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
event.save
end
end
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
current_events.values.flatten.each(&:destroy)
cache_child_event_ranges!
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
@child_event_data = nil
end
def hidden?
!appointment_group? && child_events.size > 0
end
def effective_context
effective_context_code && ActiveRecord::Base.find_by_asset_string(effective_context_code) || context
end
scope :order_by_start_at, order(:start_at)
scope :active, where("calendar_events.workflow_state<>'deleted'")
scope :are_locked, where(:workflow_state => 'locked')
scope :are_unlocked, where("calendar_events.workflow_state NOT IN ('deleted', 'locked')")
# controllers/apis/etc. should generally use for_user_and_context_codes instead
scope :for_context_codes, lambda { |codes| where(:context_code => codes) }
# appointments and appointment_participants have the appointment_group and
# the user as the context, respectively. we are actually interested in
# grouping them under the effective context (i.e. appointment_group.context).
# it's the responsibility of the caller to ensure the user has rights to the
# specified codes (e.g. using User#appointment_context_codes)
scope :for_user_and_context_codes, lambda { |user, *args|
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
codes = args.shift
section_codes = args.shift || user.section_context_codes(codes)
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
effectively_courses_codes = [user.asset_string] + section_codes
# the all_codes check is redundant, but makes the query more efficient
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
all_codes = codes | effectively_courses_codes
group_codes = codes.grep(/\Aappointment_group_\d+\z/)
codes -= group_codes
codes_conditions = codes.map { |code|
wildcard(quoted_table_name + '.effective_context_code', code, :delimiter => ',')
}.join(" OR ")
codes_conditions = self.connection.quote(false) if codes_conditions.blank?
where(<<-SQL, all_codes, codes, group_codes, effectively_courses_codes)
calendar_events.context_code IN (?)
AND (
( -- explicit contexts (e.g. course_123)
calendar_events.context_code IN (?)
AND calendar_events.effective_context_code IS NULL
)
OR ( -- appointments (manageable or reservable)
calendar_events.context_code IN (?)
)
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
OR ( -- own appointment_participants, or section events in the course
calendar_events.context_code IN (?)
AND (#{codes_conditions})
)
)
SQL
}
scope :undated, where(:start_at => nil, :end_at => nil)
scope :between, lambda { |start, ending| where(:start_at => start..ending) }
scope :current, lambda { where("calendar_events.end_at>=?", Time.zone.now.midnight) }
scope :updated_after, lambda { |*args|
2011-02-01 09:57:29 +08:00
if args.first
where("calendar_events.updated_at IS NULL OR calendar_events.updated_at>?", args.first)
else
scoped
2011-02-01 09:57:29 +08:00
end
}
scope :events_without_child_events, where("NOT EXISTS (SELECT 1 FROM calendar_events children WHERE children.parent_calendar_event_id = calendar_events.id AND children.workflow_state<>'deleted')")
scope :events_with_child_events, where("EXISTS (SELECT 1 FROM calendar_events children WHERE children.parent_calendar_event_id = calendar_events.id AND children.workflow_state<>'deleted')")
def validate_context!
@validate_context = true
context.validation_event_override = self
end
attr_reader :validate_context
2011-02-01 09:57:29 +08:00
def default_values
self.context_code = "#{self.context_type.underscore}_#{self.context_id}"
self.title ||= (self.context_type.to_s + " Event") rescue "Event"
populate_missing_dates
populate_all_day_flag unless self.imported
if parent_event
populate_with_parent_event
elsif context.is_a?(AppointmentGroup)
populate_appointment_group_defaults
end
end
protected :default_values
def populate_appointment_group_defaults
self.effective_context_code = context.appointment_group_contexts.map(&:context_code).join(",")
if new_record?
AppointmentGroup::EVENT_ATTRIBUTES.each { |attr| send("#{attr}=", attr == :description ? context.description_html : context.send(attr)) }
if locked?
self.start_at = start_at_was if !new_record? && start_at_changed?
self.end_at = end_at_was if !new_record? && end_at_changed?
end
else
# we only allow changing the description
(AppointmentGroup::EVENT_ATTRIBUTES - [:description]).each { |attr| send("#{attr}=", send("#{attr}_was")) if send("#{attr}_changed?") }
end
end
protected :populate_appointment_group_defaults
def populate_with_parent_event
self.effective_context_code = if appointment_group # appointment participant
appointment_group.appointment_group_contexts.map(&:context_code).join(',') if appointment_group.participant_type == 'User'
else # e.g. section-level event
parent_event.context_code
end
(locked? ? LOCKED_ATTRIBUTES : CASCADED_ATTRIBUTES).each{ |attr| send("#{attr}=", parent_event.send(attr)) }
end
protected :populate_with_parent_event
# Populate the start and end dates if they are not set, or if they are invalid
def populate_missing_dates
2011-02-01 09:57:29 +08:00
self.end_at ||= self.start_at
self.start_at ||= self.end_at
if self.start_at && self.end_at && self.end_at < self.start_at
2011-02-01 09:57:29 +08:00
self.end_at = self.start_at
end
end
protected :populate_missing_dates
def populate_all_day_flag
# If the all day flag has been changed to all day, set the times to 00:00
if self.all_day_changed? && self.all_day?
self.start_at = self.end_at = zoned_start_at.beginning_of_day rescue nil
elsif self.start_at_changed? || self.end_at_changed?
2011-02-01 09:57:29 +08:00
if self.start_at && self.start_at == self.end_at && zoned_start_at.strftime("%H:%M") == '00:00'
self.all_day = true
else
self.all_day = false
end
end
if self.all_day && (!self.all_day_date || self.start_at_changed? || self.all_day_date_changed?)
self.start_at = self.end_at = zoned_start_at.beginning_of_day rescue nil
self.all_day_date = (zoned_start_at.to_date rescue nil)
end
2011-02-01 09:57:29 +08:00
end
protected :populate_all_day_flag
# Localized start_at
def zoned_start_at
self.start_at && ActiveSupport::TimeWithZone.new(self.start_at.utc,
((ActiveSupport::TimeZone.new(self.time_zone_edited) rescue nil) || Time.zone))
end
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
CASCADED_ATTRIBUTES = [
:title,
:description,
:location_name,
:location_address
]
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
LOCKED_ATTRIBUTES = CASCADED_ATTRIBUTES + [
:start_at,
:end_at
]
section-level calendar event support (backend/api), fixes #7979 added a child_event_data attribute that can be set during create and update for specifying child events. the initial use case is for section- level events under a course. other context types will return an error child_event_data should be an array of objects representing the child events. each object must have a context_code and may have a start_at/ end_at, e.g. child_event_data[0][context_code]=course_section_1&... when updating a calendar event, child_event_data will replace previous child events (creating/updating/deleting, as appropriate) to delete all child events when updating, set remove_child_events=1 when querying events for a given course (via index), child events for the course's sections will now appear (limited to the user's visibility) test plan: 1. create a calendar event via the api and specify child events 2. confirm that they were created correctly 3. confirm that the index action returns those child events when searching by course 4. update the calendar event and change the child events 5. confirm the child events were deleted successfully 6. update the calendar event and delete all child events (via remove_child_events=1) 7. confirm the child events are deleted 8. delete an event that has child events 9. confirm that the child events are deleted as well Change-Id: If94b17a9c33ca3a56f7b97908c8c659e12364d8b Reviewed-on: https://gerrit.instructure.com/10021 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com>
2012-04-13 08:27:52 +08:00
def sync_child_events
locked_changes = LOCKED_ATTRIBUTES.select { |attr| send("#{attr}_changed?") }
cascaded_changes = CASCADED_ATTRIBUTES.select { |attr| send("#{attr}_changed?") }
child_events.are_locked.update_all Hash[locked_changes.map{ |attr| [attr, send(attr)] }] if locked_changes.present?
child_events.are_unlocked.update_all Hash[cascaded_changes.map{ |attr| [attr, send(attr)] }] if cascaded_changes.present?
end
attr_writer :skip_sync_parent_event
def sync_parent_event
return unless parent_event
return if appointment_group
return unless start_at_changed? || end_at_changed? || workflow_state_changed?
return if @skip_sync_parent_event
parent_event.cache_child_event_ranges! unless workflow_state == 'deleted'
end
def cache_child_event_ranges!
events = child_events(true)
if events.present?
CalendarEvent.where(:id => self).
update_all(:start_at => events.map(&:start_at).min,
:end_at => events.map(&:end_at).max)
reload
end
end
2011-02-01 09:57:29 +08:00
workflow do
state :active
state :locked do # locked events may only be deleted, they cannot be edited directly
event :unlock, :transitions_to => :active
end
2011-02-01 09:57:29 +08:00
state :deleted
end
2011-02-01 09:57:29 +08:00
alias_method :destroy!, :destroy
def destroy(update_context_or_parent=true)
transaction do
self.workflow_state = 'deleted'
self.deleted_at = Time.now.utc
save!
child_events.find_each do |e|
e.cancel_reason = cancel_reason
e.updating_user = updating_user
e.destroy(false)
end
return true unless update_context_or_parent
if appointment_group
context.touch if context_type == 'AppointmentGroup' # ensures end_at/start_at get updated
# when deleting an appointment or appointment_participant, make sure we reset the cache
appointment_group.clear_cached_available_slots!
end
if parent_event && parent_event.locked? && parent_event.child_events.size == 0
parent_event.workflow_state = 'active'
parent_event.save!
end
true
end
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def time_zone_edited
CGI::unescapeHTML(read_attribute(:time_zone_edited) || "")
end
2011-02-01 09:57:29 +08:00
has_a_broadcast_policy
set_broadcast_policy do
dispatch :new_event_created
to { participants - [@updating_user] }
whenever {
!appointment_group && context.available? && just_created && !hidden?
}
dispatch :event_date_changed
to { participants - [@updating_user] }
whenever {
!appointment_group &&
context.available? && (
changed_in_state(:active, :fields => :start_at) ||
changed_in_state(:active, :fields => :end_at)
) && !hidden?
}
dispatch :appointment_reserved_by_user
to { appointment_group.instructors }
whenever {
appointment_group && parent_event &&
just_created &&
context == appointment_group.participant_for(user)
}
data { {:updating_user => @updating_user} }
dispatch :appointment_canceled_by_user
to { appointment_group.instructors }
whenever {
appointment_group && parent_event &&
deleted? &&
workflow_state_changed? &&
@updating_user &&
context == appointment_group.participant_for(@updating_user)
}
data { {
:updating_user => @updating_user,
:cancel_reason => @cancel_reason
} }
dispatch :appointment_reserved_for_user
to { participants - [@updating_user] }
whenever {
appointment_group && parent_event &&
just_created
2011-02-01 09:57:29 +08:00
}
data { {:updating_user => @updating_user} }
dispatch :appointment_deleted_for_user
to { participants - [@updating_user] }
whenever {
appointment_group && parent_event &&
deleted? &&
workflow_state_changed?
2011-02-01 09:57:29 +08:00
}
data { {
:updating_user => @updating_user,
:cancel_reason => @cancel_reason
} }
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def participants
# TODO: User#participants should probably be fixed to return [self],
# then we can simplify this again
context_type == 'User' ? [context] : context.participants
end
attr_reader :updating_user
def updating_user=(user)
self.user ||= user
@updating_user = user
content_being_saved_by(user)
end
def user
read_attribute(:user) || (context_type == 'User' ? context : nil)
end
def appointment_group?
context_type == 'AppointmentGroup' || parent_event.try(:context_type) == 'AppointmentGroup'
end
def appointment_group
if parent_event.try(:context).is_a?(AppointmentGroup)
parent_event.context
elsif context_type == 'AppointmentGroup'
context
end
end
class ReservationError < StandardError; end
def reserve_for(participant, user, options = {})
raise ReservationError, "not an appointment" unless context_type == 'AppointmentGroup'
raise ReservationError, "ineligible participant" unless context.eligible_participant?(participant)
transaction do
lock! # in case two people two participants try to grab the same slot
participant.lock! # in case two people try to make a reservation for the same participant
if options[:cancel_existing]
context.reservations_for(participant).lock.each do |reservation|
reservation.updating_user = user
reservation.destroy
end
end
raise ReservationError, "participant has met per-participant limit" if context.max_appointments_per_participant && context.reservations_for(participant).size >= context.max_appointments_per_participant
raise ReservationError, "all slots filled" if participants_per_appointment && child_events.size >= participants_per_appointment
raise ReservationError, "participant has already reserved this appointment" if child_events_for(participant).present?
event = child_events.build
event.updating_user = user
event.context = participant
event.workflow_state = :locked
event.save!
if active?
self.workflow_state = 'locked'
save!
end
context.clear_cached_available_slots!
event
end
end
def child_events_for(participant)
if child_events.loaded?
child_events.select { |e| e.has_asset?(participant) }
else
child_events.where(context_type: participant.class.name, context_id: participant)
end
2011-02-01 09:57:29 +08:00
end
def participants_per_appointment
if override_participants_per_appointment?
read_attribute(:participants_per_appointment)
else
context.is_a?(AppointmentGroup) ? context.participants_per_appointment : nil
end
end
def participants_per_appointment=(limit)
# if the given limit is the same as the context's limit, we should not override
if limit == context.participants_per_appointment && override_participants_per_appointment?
self.override_participants_per_appointment = false
write_attribute(:participants_per_appointment, nil)
else
write_attribute(:participants_per_appointment, limit)
self.override_participants_per_appointment = true
end
limit
end
2011-02-01 09:57:29 +08:00
def update_matching_days=(update)
@update_matching_days = update == '1' || update == true || update == 'true'
end
2011-02-01 09:57:29 +08:00
def all_day
read_attribute(:all_day) || (self.new_record? && self.start_at && self.start_at.strftime("%H:%M") == '00:00')
end
def to_atom(opts={})
extend ApplicationHelper
Atom::Entry.new do |entry|
entry.title = t(:feed_item_title, "Calendar Event: %{event_title}", :event_title => self.title) unless opts[:include_context]
entry.title = t(:feed_item_title_with_context, "Calendar Event, %{course_or_account_name}: %{event_title}", :course_or_account_name => self.context.name, :event_title => self.title) if opts[:include_context]
entry.authors << Atom::Person.new(:name => self.context.name)
2011-02-01 09:57:29 +08:00
entry.updated = self.updated_at.utc
entry.published = self.created_at.utc
entry.links << Atom::Link.new(:rel => 'alternate',
2011-02-01 09:57:29 +08:00
:href => "http://#{HostUrl.context_host(self.context)}/#{context_url_prefix}/calendar?month=#{self.start_at.strftime("%m") rescue ""}&year=#{self.start_at.strftime("%Y") rescue ""}#calendar_event_#{self.id}")
entry.id = "tag:#{HostUrl.default_host},#{self.created_at.strftime("%Y-%m-%d")}:/calendar_events/#{self.feed_code}_#{self.start_at.strftime("%Y-%m-%d-%H-%M") rescue "none"}_#{self.end_at.strftime("%Y-%m-%d-%H-%M") rescue "none"}"
entry.content = Atom::Content::Html.new("#{datetime_string(self.start_at, self.end_at)}<br/>#{self.description}")
end
end
2011-02-01 09:57:29 +08:00
def to_ics(in_own_calendar=true)
return CalendarEvent::IcalEvent.new(self).to_ics(in_own_calendar)
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def self.process_migration(data, migration)
events = data['calendar_events'] ? data['calendar_events']: []
events.each do |event|
if migration.import_object?("calendar_events", event['migration_id']) || migration.import_object?("events", event['migration_id'])
begin
import_from_migration(event, migration.context)
rescue
migration.add_import_warning(t('#migration.calendar_event_type', "Calendar Event"), event[:title], $!)
end
2011-02-01 09:57:29 +08:00
end
end
end
2011-02-01 09:57:29 +08:00
def self.import_from_migration(hash, context, item=nil)
hash = hash.with_indifferent_access
return nil if hash[:migration_id] && hash[:events_to_import] && !hash[:events_to_import][hash[:migration_id]]
item ||= find_by_context_type_and_context_id_and_id(context.class.to_s, context.id, hash[:id])
item ||= find_by_context_type_and_context_id_and_migration_id(context.class.to_s, context.id, hash[:migration_id]) if hash[:migration_id]
item ||= context.calendar_events.new
Importers::CalendarEvent.import_from_migration(hash, context, item)
2011-02-01 09:57:29 +08:00
end
2011-02-01 09:57:29 +08:00
def self.max_visible_calendars
10
end
set_policy do
given { |user, session| self.cached_context_grants_right?(user, session, :read) }#students.include?(user) }
can :read
given { |user, session| !appointment_group? ^ cached_context_grants_right?(user, session, :read_appointment_participants) }
can :read_child_events
given { |user, session| parent_event && appointment_group? && parent_event.grants_right?(user, session, :manage) }
can :read and can :delete
given { |user, session| appointment_group? && cached_context_grants_right?(user, session, :manage) }
can :manage
given { |user, session|
appointment_group? && (
grants_right?(user, session, :manage) ||
cached_context_grants_right?(user, nil, :reserve) && context.participant_for(user).present?
)
}
can :reserve
2011-02-01 09:57:29 +08:00
given { |user, session| self.cached_context_grants_right?(user, session, :manage_calendar) }#admins.include?(user) }
can :read and can :create
given { |user, session| (!locked? || context.is_a?(AppointmentGroup)) && !deleted? && self.cached_context_grants_right?(user, session, :manage_calendar) }#admins.include?(user) }
can :update and can :update_content
given { |user, session| !deleted? && self.cached_context_grants_right?(user, session, :manage_calendar) }
can :delete
2011-02-01 09:57:29 +08:00
end
class IcalEvent
include Api
if CANVAS_RAILS2
include ActionController::UrlWriter
else
include Rails.application.routes.url_helpers
end
include HtmlTextHelper
def initialize(event)
@event = event
end
def location
end
def to_ics(in_own_calendar)
cal = Icalendar::Calendar.new
# to appease Outlook
cal.custom_property("METHOD","PUBLISH")
event = Icalendar::Event.new
event.klass = "PUBLIC"
start_at = @event.is_a?(CalendarEvent) ? @event.start_at : @event.due_at
end_at = @event.is_a?(CalendarEvent) ? @event.end_at : @event.due_at
if start_at
event.start = start_at.utc_datetime
event.start.icalendar_tzid = 'UTC'
end
if end_at
event.end = end_at.utc_datetime
event.end.icalendar_tzid = 'UTC'
end
if @event.all_day
event.start = Date.new(@event.all_day_date.year, @event.all_day_date.month, @event.all_day_date.day)
event.start.ical_params = {"VALUE"=>["DATE"]}
event.end = event.start
event.end.ical_params = {"VALUE"=>["DATE"]}
end
event.summary = @event.title
if @event.is_a?(CalendarEvent) && @event.description
html = api_user_content(@event.description, @event.context)
event.description html_to_text(html)
event.x_alt_desc(html, { 'FMTTYPE' => 'text/html' })
end
if @event.is_a?(CalendarEvent)
loc_string = ""
loc_string << @event.location_name + ", " if @event.location_name.present?
loc_string << @event.location_address if @event.location_address.present?
else
loc_string = @event.location
end
event.location = loc_string
event.dtstamp = @event.updated_at.utc_datetime if @event.updated_at
event.dtstamp.icalendar_tzid = 'UTC' if event.dtstamp
tag_name = @event.class.name.underscore
# This will change when there are other things that have calendars...
# can't call calendar_url or calendar_url_for here, have to do it manually
event.url "http://#{HostUrl.context_host(@event.context)}/calendar?include_contexts=#{@event.context.asset_string}&month=#{start_at.try(:strftime, "%m")}&year=#{start_at.try(:strftime, "%Y")}##{tag_name}_#{@event.id.to_s}"
event.uid "event-#{tag_name.gsub('_', '-')}-#{@event.id.to_s}"
event.sequence 0
if @event.respond_to?(:applied_overrides)
@event.applied_overrides.try(:each) do |override|
next unless override.due_at_overridden
tag_name = override.class.name.underscore
event.uid = "event-#{tag_name.gsub('_', '-')}-#{override.id}"
event.summary = "#{@event.title} (#{override.title})"
#TODO: event.url
end
end
# make an effort to find an associated course without diving too deep down the rabbit hole
associated_course = nil
if @event.is_a?(CalendarEvent)
if @event.effective_context.is_a?(Course)
associated_course = @event.effective_context
elsif @event.effective_context.respond_to?(:context) && @event.effective_context.context.is_a?(Course)
associated_course = @event.effective_context.context
end
elsif @event.respond_to?(:context) && @event.context_type == "Course"
associated_course = @event.context
end
event.summary += " [#{associated_course.course_code}]" if associated_course
event = nil unless start_at
return event unless in_own_calendar
cal.add_event(event) if event
return cal.to_ical
end
end
2011-02-01 09:57:29 +08:00
end