section-level event support in calendar2, refs #7978
respect "hidden" flag such that we don't show parent events of section- level events in the calendar or upcoming events. when viewing the section-level event's details, show the section name. when editing a section-level event, don't allow changing the title. link to parent event when clicking "more options" test plan: 1. create an event with section-level child events (currently only possible via api or console) 2. go the course calendar (calendar2) 1. confirm that the parent event is not visible 2. confirm that the section-level events are visible 3. click on a section-level event 4. confirm that the section name appears in the details 5. edit a section-level event 6. confirm that you can change the date/times but not the title 7. click the more options link and confirm it takes you to the parent event 3. go to the dashboard 1. confirm that the parent event is not visible in upcoming events 2. confirm that the section level-events are visible Change-Id: I8eadc53b7c47922f753625930d94d4f08386c817 Reviewed-on: https://gerrit.instructure.com/10116 Tested-by: Hudson <hudson@instructure.com> Reviewed-by: Ryan Shaw <ryan@instructure.com> Reviewed-by: Cameron Matheson <cameron@instructure.com>
This commit is contained in:
parent
0a6a5ca9d9
commit
94240ba544
|
@ -9,8 +9,8 @@ define [
|
|||
deleteConfirmation = I18n.t('prompts.delete_event', "Are you sure you want to delete this event?")
|
||||
|
||||
class CalendarEvent extends CommonEvent
|
||||
constructor: (data, contextInfo) ->
|
||||
super data, contextInfo
|
||||
constructor: (data, contextInfo, actualContextInfo) ->
|
||||
super data, contextInfo, actualContextInfo
|
||||
@eventType = 'calendar_event'
|
||||
@deleteConfirmation = deleteConfirmation
|
||||
@deleteURL = contextInfo.calendar_event_url
|
||||
|
@ -26,6 +26,7 @@ define [
|
|||
@end = if data.end_at then $.parseFromISO(data.end_at).time else null
|
||||
@allDay = data.all_day
|
||||
@editable = true
|
||||
@lockedTitle = @object.parent_event_id?
|
||||
@addClass "group_#{@contextCode()}"
|
||||
if @isAppointmentGroupEvent()
|
||||
@addClass "scheduler-event"
|
||||
|
@ -64,11 +65,8 @@ define [
|
|||
methodAndURLForSave: () ->
|
||||
if @isNewEvent()
|
||||
method = 'POST'
|
||||
url = @contextInfo.create_calendar_event_url
|
||||
url = '/api/v1/calendar_events'
|
||||
else
|
||||
method = 'PUT'
|
||||
url = if @isAppointmentGroupEvent()
|
||||
$.replaceTags @calendarEvent.url, 'id', @calendarEvent.id
|
||||
else
|
||||
$.replaceTags @contextInfo.calendar_event_url, 'id', @object.id
|
||||
url = @calendarEvent.url
|
||||
[ method, url ]
|
||||
|
|
|
@ -5,9 +5,10 @@ define [
|
|||
], ($) ->
|
||||
|
||||
class
|
||||
constructor: (data, contextInfo) ->
|
||||
constructor: (data, contextInfo, actualContextInfo) ->
|
||||
@eventType = 'generic'
|
||||
@contextInfo = contextInfo
|
||||
@actualContextInfo = actualContextInfo
|
||||
@allPossibleContexts = null
|
||||
@className = []
|
||||
@object = {}
|
||||
|
|
|
@ -16,6 +16,7 @@ define [
|
|||
@form = $(editCalendarEventTemplate({
|
||||
title: @event.title
|
||||
contexts: @event.possibleContexts()
|
||||
lockedTitle: @event.lockedTitle
|
||||
}))
|
||||
$(selector).append @form
|
||||
|
||||
|
@ -29,8 +30,6 @@ define [
|
|||
# Hide the context selector completely if this is an existing event, since it can't be changed.
|
||||
if !@event.isNewEvent()
|
||||
@form.find(".context_select").hide()
|
||||
@form.attr('method', 'PUT')
|
||||
@form.attr('action', $.replaceTags(@event.contextInfo.calendar_event_url, 'id', @event.object.id))
|
||||
|
||||
contextInfoForCode: (code) ->
|
||||
for context in @event.possibleContexts()
|
||||
|
@ -42,6 +41,7 @@ define [
|
|||
@form.find("select.context_id").change()
|
||||
|
||||
moreOptionsClick: (jsEvent) =>
|
||||
return if @event.object.parent_event_id
|
||||
jsEvent.preventDefault()
|
||||
pieces = $(jsEvent.target).attr('href').split("#")
|
||||
data = $("#edit_calendar_event_form").getFormData(object_name: 'calendar_event')
|
||||
|
@ -69,10 +69,9 @@ define [
|
|||
# Update the edit and more option urls
|
||||
moreOptionsHref = null
|
||||
if @event.isNewEvent()
|
||||
@form.attr('action', @currentContextInfo.create_calendar_event_url)
|
||||
moreOptionsHref = @currentContextInfo.new_calendar_event_url
|
||||
else
|
||||
moreOptionsHref = $.replaceTags(@currentContextInfo.calendar_event_url, 'id', @event.object.id)
|
||||
moreOptionsHref = $.replaceTags(@currentContextInfo.calendar_event_url, 'id', @event.object.parent_event_id ? @event.object.id)
|
||||
moreOptionsHref += '/edit'
|
||||
@form.find(".more_options_link").attr 'href', moreOptionsHref
|
||||
|
||||
|
@ -130,12 +129,13 @@ define [
|
|||
end_date = null
|
||||
|
||||
params = {
|
||||
'calendar_event[title]': data.title
|
||||
'calendar_event[title]': data.title ? @event.title
|
||||
'calendar_event[start_at]': if start_date then $.dateToISO8601UTC($.unfudgeDateForProfileTimezone(start_date)) else ''
|
||||
'calendar_event[end_at]': if end_date then $.dateToISO8601UTC($.unfudgeDateForProfileTimezone(end_date)) else ''
|
||||
}
|
||||
|
||||
if @event.isNewEvent()
|
||||
params['calendar_event[context_code]'] = data.context_code
|
||||
objectData =
|
||||
calendar_event:
|
||||
title: params['calendar_event[title]']
|
||||
|
|
|
@ -3,7 +3,8 @@ define [
|
|||
'compiled/calendar/CommonEvent'
|
||||
'compiled/calendar/CommonEvent.Assignment',
|
||||
'compiled/calendar/CommonEvent.CalendarEvent'
|
||||
], ($, CommonEvent, Assignment, CalendarEvent) ->
|
||||
'compiled/str/splitAssetString'
|
||||
], ($, CommonEvent, Assignment, CalendarEvent, splitAssetString) ->
|
||||
|
||||
(data, contexts) ->
|
||||
if data == null
|
||||
|
@ -11,7 +12,8 @@ define [
|
|||
obj.allPossibleContexts = contexts
|
||||
return obj
|
||||
|
||||
context_code = data.effective_context_code || data.context_code
|
||||
actualContextCode = data.context_code
|
||||
contextCode = data.effective_context_code || actualContextCode
|
||||
|
||||
type = null
|
||||
if data.assignment || data.assignment_group_id
|
||||
|
@ -20,11 +22,13 @@ define [
|
|||
type = 'calendar_event'
|
||||
|
||||
data = data.assignment || data.calendar_event || data
|
||||
context_code ?= data.effective_context_code || data.context_code
|
||||
return null if data.hidden # e.g. parent event of section-level events
|
||||
actualContextCode ?= data.context_code
|
||||
contextCode ?= data.effective_context_code || data.context_code
|
||||
|
||||
contextInfo = null
|
||||
for context in contexts
|
||||
if context.asset_string == context_code
|
||||
if context.asset_string == contextCode
|
||||
contextInfo = context
|
||||
break
|
||||
|
||||
|
@ -33,10 +37,14 @@ define [
|
|||
if contextInfo == null
|
||||
return null
|
||||
|
||||
parts = splitAssetString(actualContextCode) if actualContextCode isnt contextCode
|
||||
actualContextInfo = if parts and items = contextInfo[parts[0]]
|
||||
(item for item in items when item.id is parts[1])[0]
|
||||
|
||||
if type == 'assignment'
|
||||
obj = new Assignment(data, contextInfo)
|
||||
else
|
||||
obj = new CalendarEvent(data, contextInfo)
|
||||
obj = new CalendarEvent(data, contextInfo, actualContextInfo)
|
||||
|
||||
# TODO: Improve permissions handling
|
||||
# The API is not currently telling us what permissions a user
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
define ['str/pluralize'], (pluralize) ->
|
||||
(assetString) ->
|
||||
if match = assetString.match(/(.*)_(\d+)$/)
|
||||
[pluralize(match[1]), parseInt(match[2])]
|
|
@ -80,7 +80,7 @@ class CalendarsController < ApplicationController
|
|||
:assignment_groups => context.respond_to?("assignments") ? context.assignment_groups.active.scoped(:select => "id, name").map {|g| { :id => g.id, :name => g.name } } : [],
|
||||
:can_create_appointment_groups => context.respond_to?("appointment_groups") && context.appointment_groups.new.grants_right?(@current_user, session, :create),
|
||||
}
|
||||
if info[:can_create_appointment_groups] && context.respond_to?("course_sections")
|
||||
if context.respond_to?("course_sections")
|
||||
info[:course_sections] = context.course_sections.active.scoped(:select => "id, name").map {|cs| { :id => cs.id, :asset_string => cs.asset_string, :name => cs.name } }
|
||||
end
|
||||
if info[:can_create_appointment_groups] && context.respond_to?("group_categories")
|
||||
|
|
|
@ -27,6 +27,7 @@ class AppointmentGroup < ActiveRecord::Base
|
|||
has_many :_appointments, opts.merge(:conditions => opts[:conditions].gsub(/calendar_events\./, 'calendar_events_join.'))
|
||||
has_many :appointments_participants, :through => :_appointments, :source => :child_events, :conditions => "calendar_events.workflow_state <> 'deleted'", :order => :start_at
|
||||
belongs_to :context, :polymorphic => true
|
||||
alias_method :effective_context, :context
|
||||
belongs_to :sub_context, :polymorphic => true
|
||||
|
||||
before_validation :default_values
|
||||
|
|
|
@ -46,6 +46,7 @@ class Assignment < ActiveRecord::Base
|
|||
has_one :rubric, :through => :rubric_association
|
||||
has_one :teacher_enrollment, :class_name => 'TeacherEnrollment', :foreign_key => 'course_id', :primary_key => 'context_id', :include => :user, :conditions => ['enrollments.workflow_state = ?', 'active']
|
||||
belongs_to :context, :polymorphic => true
|
||||
alias_method :effective_context, :context
|
||||
belongs_to :cloned_item
|
||||
belongs_to :grading_standard
|
||||
belongs_to :group_category
|
||||
|
|
|
@ -43,6 +43,7 @@ class CalendarEvent < ActiveRecord::Base
|
|||
before_save :default_values
|
||||
after_save :touch_context
|
||||
after_save :replace_child_events
|
||||
after_save :sync_parent_event
|
||||
after_update :sync_child_events
|
||||
|
||||
# when creating/updating a calendar_event, you can give it a list of child
|
||||
|
@ -83,18 +84,23 @@ class CalendarEvent < ActiveRecord::Base
|
|||
context = @child_event_contexts[data[:context_code]][0]
|
||||
event = child_events.build(:start_at => data[:start_at], :end_at => data[:end_at])
|
||||
event.context = context
|
||||
event.skip_sync_parent_event = true
|
||||
event.save
|
||||
end
|
||||
end
|
||||
current_events.values.flatten.each(&:destroy)
|
||||
child_events.reload
|
||||
CalendarEvent.update_all({:start_at => child_events.map(&:start_at).min,
|
||||
:end_at => child_events.map(&:end_at).max
|
||||
}, ["id = ?", id])
|
||||
reload
|
||||
cache_child_event_ranges!
|
||||
@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
|
||||
|
||||
named_scope :active, :conditions => ['calendar_events.workflow_state != ?', 'deleted']
|
||||
named_scope :locked, :conditions => ["calendar_events.workflow_state = 'locked'"]
|
||||
named_scope :unlocked, :conditions => ['calendar_events.workflow_state NOT IN (?)', ['deleted', 'locked']]
|
||||
|
@ -111,7 +117,7 @@ class CalendarEvent < ActiveRecord::Base
|
|||
# specified codes (e.g. using User#appointment_context_codes)
|
||||
named_scope :for_user_and_context_codes, lambda { |user, *args|
|
||||
codes = args.shift
|
||||
section_codes = args.shift || []
|
||||
section_codes = args.shift || user.section_context_codes(codes)
|
||||
effectively_courses_codes = [user.asset_string] + section_codes
|
||||
# the all_codes check is redundant, but makes the query more efficient
|
||||
all_codes = codes | effectively_courses_codes
|
||||
|
@ -217,6 +223,23 @@ class CalendarEvent < ActiveRecord::Base
|
|||
child_events.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!
|
||||
end
|
||||
|
||||
def cache_child_event_ranges!
|
||||
events = child_events(true)
|
||||
CalendarEvent.update_all({:start_at => events.map(&:start_at).min,
|
||||
:end_at => events.map(&:end_at).max
|
||||
}, ["id = ?", id])
|
||||
reload
|
||||
end
|
||||
|
||||
workflow do
|
||||
state :active
|
||||
state :locked do # locked events may only be deleted, they cannot be edited directly
|
||||
|
|
|
@ -1644,7 +1644,7 @@ class User < ActiveRecord::Base
|
|||
ev = CalendarEvent
|
||||
ev = CalendarEvent.active if !opts[:include_deleted_events]
|
||||
event_codes = context_codes + AppointmentGroup.manageable_by(self, context_codes).intersecting(opts[:start_at], opts[:end_at]).map(&:asset_string)
|
||||
events += ev.for_user_and_context_codes(self, event_codes).between(opts[:start_at], opts[:end_at]).updated_after(opts[:updated_at])
|
||||
events += ev.for_user_and_context_codes(self, event_codes, []).between(opts[:start_at], opts[:end_at]).updated_after(opts[:updated_at])
|
||||
events += Assignment.active.for_context_codes(context_codes).due_between(opts[:start_at], opts[:end_at]).updated_after(opts[:updated_at]).with_just_calendar_attributes
|
||||
events.sort_by{|e| [e.start_at, e.title || ""] }.uniq
|
||||
end
|
||||
|
@ -1656,7 +1656,7 @@ class User < ActiveRecord::Base
|
|||
opts[:end_at] ||= 1.weeks.from_now
|
||||
opts[:limit] ||= 20
|
||||
|
||||
events = CalendarEvent.active.for_user_and_context_codes(self, context_codes).between(Time.now.utc, opts[:end_at]).scoped(:limit => opts[:limit])
|
||||
events = CalendarEvent.active.for_user_and_context_codes(self, context_codes).between(Time.now.utc, opts[:end_at]).scoped(:limit => opts[:limit]).reject(&:hidden?)
|
||||
events += Assignment.active.for_context_codes(context_codes).due_between(Time.now.utc, opts[:end_at]).scoped(:limit => opts[:limit]).include_submitted_count
|
||||
events += AppointmentGroup.manageable_by(self, context_codes).intersecting(Time.now.utc, opts[:end_at]).scoped(:limit => opts[:limit])
|
||||
events.sort_by{|e| [e.start_at, e.title] }.uniq.first(opts[:limit])
|
||||
|
@ -1668,7 +1668,7 @@ class User < ActiveRecord::Base
|
|||
return [] if (!context_codes || context_codes.empty?)
|
||||
|
||||
undated_events = []
|
||||
undated_events += CalendarEvent.active.for_user_and_context_codes(self, context_codes).undated.updated_after(opts[:updated_at])
|
||||
undated_events += CalendarEvent.active.for_user_and_context_codes(self, context_codes, []).undated.updated_after(opts[:updated_at])
|
||||
undated_events += Assignment.active.for_context_codes(context_codes).undated.updated_after(opts[:updated_at]).with_just_calendar_attributes
|
||||
undated_events.sort_by{|e| e.title }
|
||||
end
|
||||
|
@ -1739,6 +1739,14 @@ class User < ActiveRecord::Base
|
|||
end
|
||||
memoize :conversation_context_codes
|
||||
|
||||
def section_context_codes(context_codes)
|
||||
course_ids = context_codes.grep(/\Acourse_\d+\z/).map{ |s| s.sub(/\Acourse_/, '').to_i }
|
||||
return [] unless course_ids.present?
|
||||
Course.find_all_by_id(course_ids).inject([]) do |ary, course|
|
||||
ary.concat course.sections_visible_to(self).map(&:asset_string)
|
||||
end
|
||||
end
|
||||
|
||||
def manageable_courses(include_concluded = false)
|
||||
Course.manageable_by_user(self.id, include_concluded).not_deleted
|
||||
end
|
||||
|
|
|
@ -153,6 +153,10 @@
|
|||
max-height: 225px
|
||||
&:last-child
|
||||
border-bottom: none
|
||||
.event-details-actual-context
|
||||
font-size: 0.8em
|
||||
font-style: italic
|
||||
color: #666
|
||||
|
||||
#attendees li
|
||||
+name_bubbles
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
<span style="display: block;"><%= datetime_string(recent_event.start_at, :event, recent_event.end_at) %></span>
|
||||
<% end %>
|
||||
<% if show_context %>
|
||||
<span style="display: block; font-size: 0.8em;"><%= recent_event.context.short_name %></span>
|
||||
<span style="display: block; font-size: 0.8em;"><%= recent_event.effective_context.short_name %></span>
|
||||
<% end %>
|
||||
</span>
|
||||
</span>
|
||||
|
|
|
@ -3,7 +3,11 @@
|
|||
<tr>
|
||||
<td style="vertical-align: top;">{{#t "title"}}Title:{{/t}}</td>
|
||||
<td>
|
||||
<input id="calendar_event_title" name="calendar_event[title]" size="30" style="width: 150px;" type="text" value="{{title}}"/>
|
||||
{{#if lockedTitle}}
|
||||
{{title}}
|
||||
{{else}}
|
||||
<input id="calendar_event_title" name="calendar_event[title]" size="30" style="width: 150px;" type="text" value="{{title}}"/>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -22,7 +26,7 @@
|
|||
<tr class="context_select">
|
||||
<td>{{#t "calendar"}}Calendar:{{/t}}</td>
|
||||
<td>
|
||||
<select class="context_id">
|
||||
<select class="context_id" name="calendar_event[context_code]">
|
||||
{{#each contexts}}
|
||||
{{#if can_create_calendar_events}}
|
||||
<option value="{{asset_string}}">{{name}}</option>
|
||||
|
|
|
@ -15,7 +15,11 @@
|
|||
{{#if contextInfo}}
|
||||
<tr>
|
||||
<th scope="row">{{#t "calendar"}}Calendar{{/t}}</th>
|
||||
<td><a href="{{contextInfo.url}}">{{contextInfo.name}}</a></td>
|
||||
<td><a href="{{contextInfo.url}}">{{contextInfo.name}}</a>
|
||||
{{#if actualContextInfo}}
|
||||
<br><span class="event-details-actual-context">{{actualContextInfo.name}}</span>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/if}}
|
||||
{{#if location_name}}
|
||||
|
|
|
@ -36,7 +36,7 @@ class ActiveRecord::Base
|
|||
255
|
||||
end
|
||||
|
||||
def self.find_by_asset_string(string, asset_types)
|
||||
def self.find_by_asset_string(string, asset_types=nil)
|
||||
find_all_by_asset_string([string], asset_types)[0]
|
||||
end
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ module Api::V1::CalendarEvent
|
|||
hash['effective_context_code'] = event.effective_context_code if event.effective_context_code
|
||||
hash["child_events_count"] = event.child_events.size
|
||||
hash['parent_event_id'] = event.parent_calendar_event_id
|
||||
hash['hidden'] = (hash["child_events_count"] > 0 && !event.appointment_group)
|
||||
hash['hidden'] = event.hidden?
|
||||
|
||||
if include.include?('participants')
|
||||
if event.context_type == 'User'
|
||||
|
|
|
@ -179,12 +179,9 @@ describe CalendarEvent do
|
|||
should eql [] # none of the appointments even though they technically are on the section
|
||||
|
||||
CalendarEvent.for_user_and_context_codes(@student, [@course.asset_string, @student.asset_string]).sort_by(&:id).
|
||||
should eql [@e1, @e2, a1, a2, pe]
|
||||
should eql [@e1, @e2, a1, a2, pe, se]
|
||||
|
||||
CalendarEvent.for_user_and_context_codes(@student, [@course.asset_string]).sort_by(&:id).
|
||||
should eql [@e1, a1, a2, pe]
|
||||
|
||||
CalendarEvent.for_user_and_context_codes(@student, [@course.asset_string], [section.asset_string]).sort_by(&:id).
|
||||
should eql [@e1, a1, a2, pe, se]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -290,6 +290,29 @@ describe "calendar2" do
|
|||
get_header_text.should == (current_month + ' ' + Time.now.year.to_s)
|
||||
end
|
||||
|
||||
it "should show section-level events, but not the parent event" do
|
||||
@course.default_section.update_attribute(:name, "default section!")
|
||||
s2 = @course.course_sections.create!(:name => "other section!")
|
||||
date = Date.today
|
||||
e1 = @course.calendar_events.build :title => "ohai",
|
||||
:child_event_data => [
|
||||
{:start_at => "#{date} 12:00:00", :end_at => "#{date} 13:00:00", :context_code => @course.default_section.asset_string},
|
||||
{:start_at => "#{date} 13:00:00", :end_at => "#{date} 14:00:00", :context_code => s2.asset_string},
|
||||
]
|
||||
e1.updating_user = @user
|
||||
e1.save!
|
||||
|
||||
get "/calendar2"
|
||||
wait_for_ajaximations
|
||||
events = ff('.fc-event')
|
||||
events.size.should eql 2
|
||||
events.first.click
|
||||
|
||||
details = f('.event-details-content')
|
||||
details.should_not be_nil
|
||||
details.text.should include(@course.default_section.name)
|
||||
end
|
||||
|
||||
context "event editing" do
|
||||
it "should allow editing appointment events" do
|
||||
create_appointment_group
|
||||
|
@ -369,6 +392,30 @@ describe "calendar2" do
|
|||
driver.find_element(:id, 'appointment-group-list').should include_text(ag.title)
|
||||
end
|
||||
end
|
||||
|
||||
it "should show section-level events for the student's section" do
|
||||
@course.default_section.update_attribute(:name, "default section!")
|
||||
s2 = @course.course_sections.create!(:name => "other section!")
|
||||
date = Date.today
|
||||
e1 = @course.calendar_events.build :title => "ohai",
|
||||
:child_event_data => [
|
||||
{:start_at => "#{date} 12:00:00", :end_at => "#{date} 13:00:00", :context_code => s2.asset_string},
|
||||
{:start_at => "#{date} 13:00:00", :end_at => "#{date} 14:00:00", :context_code => @course.default_section.asset_string},
|
||||
]
|
||||
e1.updating_user = @teacher
|
||||
e1.save!
|
||||
|
||||
get "/calendar2"
|
||||
wait_for_ajaximations
|
||||
events = ff('.fc-event')
|
||||
events.size.should eql 1
|
||||
events.first.text.should include "1p"
|
||||
events.first.click
|
||||
|
||||
details = f('.event-details-content')
|
||||
details.should_not be_nil
|
||||
details.text.should include(@course.default_section.name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Reference in New Issue