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:
Jon Jensen 2012-04-17 16:38:45 -06:00
parent 0a6a5ca9d9
commit 94240ba544
18 changed files with 138 additions and 38 deletions

View File

@ -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 ]

View File

@ -5,9 +5,10 @@ define [
], ($) ->
class
constructor: (data, contextInfo) ->
constructor: (data, contextInfo, actualContextInfo) ->
@eventType = 'generic'
@contextInfo = contextInfo
@actualContextInfo = actualContextInfo
@allPossibleContexts = null
@className = []
@object = {}

View File

@ -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]']

View File

@ -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

View File

@ -0,0 +1,4 @@
define ['str/pluralize'], (pluralize) ->
(assetString) ->
if match = assetString.match(/(.*)_(\d+)$/)
[pluralize(match[1]), parseInt(match[2])]

View File

@ -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")

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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>

View File

@ -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>

View File

@ -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}}

View File

@ -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

View File

@ -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'

View File

@ -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

View File

@ -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