formatting for ICS calendar events
Return the HTML as an ext ics attribute for those few clients that support it (Outlook), and return better-formatted plain text for all other clients. closes #9107 Also refactor a bit. test plan: add assignment and calendar events to the calendar with links, formatted text, etc in their descriptions. Export the calendar feed. In Outlook, you'll see the full HTML of the event. In most other clients, you'll see a plain-text version of the description, but it'll be much better formatted than it was before. Change-Id: I50af1c407483d84c65ca285cbf364b6a303e0379 Reviewed-on: https://gerrit.instructure.com/11891 Tested-by: Jenkins <jenkins@instructure.com> Reviewed-by: Rob Orton <rob@instructure.com>
This commit is contained in:
parent
71e88b1b85
commit
b283bc3b87
|
@ -646,40 +646,7 @@ class Assignment < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def to_ics(in_own_calendar=true)
|
||||
cal = Icalendar::Calendar.new
|
||||
# to appease Outlook
|
||||
cal.custom_property("METHOD","PUBLISH")
|
||||
|
||||
event = Icalendar::Event.new
|
||||
event.klass = "PUBLIC"
|
||||
event.start = self.due_at.utc_datetime if self.due_at
|
||||
event.start.icalendar_tzid = 'UTC' if event.start
|
||||
event.end = event.start if event.start
|
||||
event.end.icalendar_tzid = 'UTC' if event.end
|
||||
if self.all_day
|
||||
event.start = Date.new(self.all_day_date.year, self.all_day_date.month, self.all_day_date.day)
|
||||
event.start.ical_params = {"VALUE"=>["DATE"]}
|
||||
event.end = event.start
|
||||
event.end.ical_params = {"VALUE"=>["DATE"]}
|
||||
end
|
||||
event.summary = self.title
|
||||
event.description = strip_tags(self.description).strip
|
||||
event.location = self.location
|
||||
event.dtstamp = self.updated_at.utc_datetime if self.updated_at
|
||||
event.dtstamp.icalendar_tzid = 'UTC' if event.dtstamp
|
||||
# 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(self.context)}/calendar?include_contexts=#{self.context.asset_string}&month=#{self.due_at.strftime("%m") rescue ""}&year=#{self.due_at.strftime("%Y") rescue ""}#assignment_#{self.id.to_s}"
|
||||
event.uid "event-assignment-#{self.id.to_s}"
|
||||
event.sequence 0
|
||||
event = nil unless self.due_at
|
||||
|
||||
return event unless in_own_calendar
|
||||
|
||||
cal.add_event(event) if event
|
||||
|
||||
return cal.to_ical
|
||||
|
||||
return CalendarEvent::IcalEvent.new(self).to_ics(in_own_calendar)
|
||||
end
|
||||
|
||||
def all_day
|
||||
|
|
|
@ -457,44 +457,9 @@ class CalendarEvent < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def to_ics(in_own_calendar=true)
|
||||
cal = Icalendar::Calendar.new
|
||||
# to appease Outlook
|
||||
cal.custom_property("METHOD","PUBLISH")
|
||||
|
||||
loc_string = ""
|
||||
loc_string << self.location_name + ", " if !self.location_name.blank?
|
||||
loc_string << self.location_address if !self.location_address.blank?
|
||||
|
||||
event = Icalendar::Event.new
|
||||
event.klass = "PUBLIC"
|
||||
event.start = self.start_at.utc_datetime if self.start_at
|
||||
event.start.icalendar_tzid = 'UTC' if event.start
|
||||
event.end = self.end_at.utc_datetime if self.end_at
|
||||
event.end.icalendar_tzid = 'UTC' if event.end
|
||||
if self.all_day
|
||||
event.start = Date.new(self.all_day_date.year, self.all_day_date.month, self.all_day_date.day)
|
||||
event.start.ical_params = {"VALUE"=>["DATE"]}
|
||||
event.end = event.start
|
||||
event.end.ical_params = {"VALUE"=>["DATE"]}
|
||||
end
|
||||
event.summary = self.title
|
||||
event.description = strip_tags(self.description).strip
|
||||
event.location = loc_string
|
||||
event.dtstamp = self.updated_at.utc_datetime if self.updated_at
|
||||
event.dtstamp.icalendar_tzid = 'UTC' if event.dtstamp
|
||||
# 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(self.context)}/calendar?include_contexts=#{self.context.asset_string}&month=#{self.start_at.strftime("%m") rescue ""}&year=#{self.start_at.strftime("%Y") rescue ""}#calendar_event_#{self.id.to_s}"
|
||||
event.uid "event-calendar-event-#{self.id.to_s}"
|
||||
event.sequence 0
|
||||
event = nil unless self.start_at
|
||||
|
||||
return event unless in_own_calendar
|
||||
|
||||
cal.add_event(event) if event
|
||||
|
||||
return cal.to_ical
|
||||
return CalendarEvent::IcalEvent.new(self).to_ics(in_own_calendar)
|
||||
end
|
||||
|
||||
def self.search(query)
|
||||
find(:all, :conditions => wildcard('title', 'description', query))
|
||||
end
|
||||
|
@ -622,5 +587,80 @@ class CalendarEvent < ActiveRecord::Base
|
|||
given { |user, session| !deleted? && self.cached_context_grants_right?(user, session, :manage_calendar) }
|
||||
can :delete
|
||||
end
|
||||
|
||||
class IcalEvent
|
||||
include Api
|
||||
include ActionController::UrlWriter
|
||||
include TextHelper
|
||||
|
||||
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.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
|
||||
event = nil unless start_at
|
||||
|
||||
return event unless in_own_calendar
|
||||
|
||||
cal.add_event(event) if event
|
||||
|
||||
return cal.to_ical
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
Icalendar::Event.ical_property :x_alt_desc
|
|
@ -26,6 +26,22 @@ module TextHelper
|
|||
text.gsub(/<\/?[^>\n]*>/, "").gsub(/&#\d+;/) {|m| puts m; m[2..-1].to_i.chr rescue '' }.gsub(/&\w+;/, "")
|
||||
end
|
||||
|
||||
# Converts a string of html to plain text, preserving as much of the
|
||||
# formatting and information as possible
|
||||
#
|
||||
# This is still a pretty basic implementation, I'm sure we'll find ways to
|
||||
# tweak and improve it as time goes on.
|
||||
def html_to_text(html_str)
|
||||
doc = Nokogiri::HTML::DocumentFragment.parse(html_str.squeeze(" ").squeeze("\n"))
|
||||
# translate anchor tags into a markdown-style name/link combo
|
||||
doc.css('a').each { |node| next if node.text.strip == node['href']; node.replace("[#{node.text}](#{node['href']})") }
|
||||
# translate img tags into just a url to the image
|
||||
doc.css('img').each { |node| node.replace(node['src']) }
|
||||
# append a line break to br and p tags, so they retain a line break after stripping tags
|
||||
doc.css('br, p').each { |node| node.after("\n\n") }
|
||||
doc.text.strip
|
||||
end
|
||||
|
||||
def quote_clump(quote_lines)
|
||||
txt = "<div class='quoted_text_holder'><a href='#' class='show_quoted_text_link'>#{TextHelper.escape_html(I18n.t('lib.text_helper.quoted_text_toggle', "show quoted text"))}</a><div class='quoted_text' style='display: none;'>"
|
||||
txt += quote_lines.join("\n")
|
||||
|
|
|
@ -227,6 +227,21 @@ describe TextHelper do
|
|||
TextHelper.make_subject_reply_to('Re: ohai').should == 'Re: ohai'
|
||||
end
|
||||
|
||||
context ".html_to_text" do
|
||||
it "should format links in markdown-like style" do
|
||||
th.html_to_text("<a href='www.example.com'>Link</a>").should == "[Link](www.example.com)"
|
||||
th.html_to_text("<a href='www.example.com'>www.example.com</a>").should == "www.example.com"
|
||||
end
|
||||
|
||||
it "should turn images into urls" do
|
||||
th.html_to_text("<img src='http://www.example.com/a'>").should == "http://www.example.com/a"
|
||||
end
|
||||
|
||||
it "should insert newlines for ps and brs" do
|
||||
th.html_to_text("Ohai<br>Text <p>paragraph of text.</p>End").should == "Ohai\n\nText paragraph of text.\n\nEnd"
|
||||
end
|
||||
end
|
||||
|
||||
context "markdown" do
|
||||
context "safety" do
|
||||
it "should escape Strings correctly" do
|
||||
|
|
|
@ -925,24 +925,24 @@ describe Assignment do
|
|||
res.match(/DTEND;VALUE=DATE:20080903/).should_not be_nil
|
||||
end
|
||||
|
||||
it ".to_ics should return a plain-text description" do
|
||||
assignment_model(:due_at => "Sep 3 2008 12:00am", :description => <<-HTML)
|
||||
<p>
|
||||
it ".to_ics should return a plain-text description and alt html description" do
|
||||
html = %{<div>
|
||||
This assignment is due December 16th. Plz discuss the reading.
|
||||
<p> </p>
|
||||
<p> </p>
|
||||
<p> </p>
|
||||
<p> </p>
|
||||
<p>Test.</p>
|
||||
</p>
|
||||
HTML
|
||||
</div>}
|
||||
assignment_model(:due_at => "Sep 3 2008 12:00am", :description => html)
|
||||
ev = @assignment.to_ics(false)
|
||||
ev.description.should == "This assignment is due December 16th. Plz discuss the reading.
|
||||
|
||||
|
||||
|
||||
|
||||
Test."
|
||||
ev.description.should == "This assignment is due December 16th. Plz discuss the reading.\n \n\n\n Test."
|
||||
ev.x_alt_desc.should == html.strip
|
||||
end
|
||||
|
||||
it ".to_ics should run the description through api_user_content to translate links" do
|
||||
html = %{<a href="/calendar">Click!</a>}
|
||||
assignment_model(:due_at => "Sep 3 2008 12:00am", :description => html)
|
||||
ev = @assignment.to_ics(false)
|
||||
ev.description.should == "[Click!](http://localhost/calendar)"
|
||||
ev.x_alt_desc.should == %{<a href="http://localhost/calendar">Click!</a>}
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -110,21 +110,17 @@ describe CalendarEvent do
|
|||
it ".to_ics should return a plain-text description" do
|
||||
calendar_event_model(:start_at => "Sep 3 2008 12:00am", :description => <<-HTML)
|
||||
<p>
|
||||
This assignment is due December 16th. Plz discuss the reading.
|
||||
<p> </p>
|
||||
<p> </p>
|
||||
<p> </p>
|
||||
<p> </p>
|
||||
<p>Test.</p>
|
||||
This assignment is due December 16th. <b>Please</b> do the reading.
|
||||
<br/>
|
||||
<a href="www.example.com">link!</a>
|
||||
</p>
|
||||
HTML
|
||||
ev = @event.to_ics(false)
|
||||
ev.description.should == "This assignment is due December 16th. Plz discuss the reading.
|
||||
|
||||
|
||||
|
||||
|
||||
Test."
|
||||
ev.description.should == "This assignment is due December 16th. Please do the reading.
|
||||
|
||||
|
||||
[link!](www.example.com)"
|
||||
ev.x_alt_desc.should == @event.description
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue