Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds .ics endpoints for calendars, topics and posts #169

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 77 additions & 0 deletions app/controllers/discourse_calendar_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# frozen_string_literal: true

module DiscourseCalendar
class DiscourseCalendarController < ::ApplicationController
before_action :ensure_discourse_calendar_enabled
skip_before_action :check_xhr, only: [ :topic_calendar ], if: :ics_request?

def topic_calendar

@topic = Topic.find_by(id: params[:id])

if @topic && guardian.can_see?(@topic)
@events = CalendarEvent.where(topic_id: @topic.id).order(:start_date, :end_date)

respond_to do |format|
format.ics do
filename = "topic-#{@topic.id}-calendar"
response.headers['Content-Disposition'] = "attachment; filename=\"#{filename}.#{request.format.symbol}\""

render :topic_calendar
end
end
else
raise Discourse::NotFound
end
end

def post_dates
@post = Post.find_by(id: params[:id])

if @post && guardian.can_see?(@post)
@events = LocalDatesExtractor.new(@post).extract_events
respond_to do |format|
format.ics do
filename = "post-#{@post.id}-dates"
response.headers['Content-Disposition'] = "attachment; filename=\"#{filename}.#{request.format.symbol}\""

render :post_dates
end
end
else
raise Discourse::NotFound
end
end

def topic_dates
@topic = Topic.find_by(id: params[:id])

if @topic && guardian.can_see?(@topic)
@events = @topic.posts.map{|p| LocalDatesExtractor.new(p).extract_events}.flatten

respond_to do |format|
format.ics do
filename = "topic-#{@topic.id}-dates"
response.headers['Content-Disposition'] = "attachment; filename=\"#{filename}.#{request.format.symbol}\""

render :post_dates
end
end
else
raise Discourse::NotFound
end
end

private

def ensure_discourse_calendar_enabled
if !SiteSetting.calendar_enabled
raise Discourse::NotFound
end
end

def ics_request?
request.format.symbol == :ics
end
end
end
15 changes: 15 additions & 0 deletions app/views/discourse_calendar/discourse_calendar/post_dates.ics.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Discourse//<%= Discourse.current_hostname %>//<%= Discourse.full_version %>//EN
<% @events.each_with_index do |event, index| %>
BEGIN:VEVENT
UID:post_dates_#<%= index %>@<%= Discourse.current_hostname %>
DTSTAMP:<%= Time.now.utc.strftime("%Y%m%dT%H%M%SZ") %>
DTSTART:<%= event.start_date.strftime("%Y%m%dT%H%M%SZ") %>
DTEND:<%= event.end_date.presence ? event.end_date.strftime("%Y%m%dT%H%M%SZ") : (event.start_date + 1.hour).strftime("%Y%m%dT%H%M%SZ") %>
SUMMARY:<%= event.description %>
DESCRIPTION:<%= "#{event.description}\\n\\n#{Discourse.base_url}/t/-/#{event.post.topic_id}/#{event.post.post_number}" %>
URL:<%= Discourse.base_url %>/t/-/<%= event.post.topic_id %>/<%= event.post.post_number %>
END:VEVENT
<% end %>
END:VCALENDAR
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Discourse//<%= Discourse.current_hostname %>//<%= Discourse.full_version %>//EN
<% @events.each do |event| %>
BEGIN:VEVENT
UID:calendar_post_#<%= event.id %>@<%= Discourse.current_hostname %>
DTSTAMP:<%= Time.now.utc.strftime("%Y%m%dT%H%M%SZ") %>
DTSTART:<%= event.start_date.strftime("%Y%m%dT%H%M%SZ") %>
DTEND:<%= event.end_date.presence ? event.end_date.strftime("%Y%m%dT%H%M%SZ") : (event.start_date + 1.day).strftime("%Y%m%dT%H%M%SZ") %>
SUMMARY:<%= event.description %>
DESCRIPTION:<%= "#{PrettyText.format_for_email(event.post.excerpt, event.post).html_safe}\\n\\n#{Discourse.base_url}/t/-/#{event.post.topic_id}/#{event.post.post_number}" %>
URL:<%= Discourse.base_url %>/t/-/<%= event.post.topic_id %>/<%= event.post.post_number %>
END:VEVENT
<% end %>
END:VCALENDAR
70 changes: 70 additions & 0 deletions lib/local_dates_extractor.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# frozen_string_literal: true

class LocalDatesExtractor
Event = Struct.new(:start_date, :end_date, :description, :post)

def initialize(post)
@post = post
end

def extract_events

events = []

Loofah.fragment(@post.cooked).css('span.discourse-local-date').map{|d| d.parent}.uniq.map do |paragraph_with_date|
next if paragraph_with_date.ancestors("aside").length > 0

event = Event.new
dates = paragraph_with_date.css('span.discourse-local-date')

# detects date ranges
if dates.count == 2 && paragraph_with_date.content.include?(' → ')
to, from = dates.each do |cooked_date|
date = {}
cooked_date.attributes.values.each do |attribute|
data_name = attribute.name&.gsub('data-', '')
if data_name && %w[date time timezone recurring].include?(data_name)
unless attribute.value == 'undefined'
date[data_name] = CGI.escapeHTML(attribute.value || '')
end
end
end
end
event.start_date = LocalDatesExtractor.convert_to_date_time(from)
event.end_date = LocalDatesExtractor.convert_to_date_time(to)
else #no ranges
date = {}
paragraph_with_date.css('span.discourse-local-date').attributes.values.each do |attribute|
data_name = attribute.name&.gsub('data-', '')
if data_name && %w[date time timezone recurring].include?(data_name)
unless attribute.value == 'undefined'
date[data_name] = CGI.escapeHTML(attribute.value || '')
end
end
end

event.start_date = LocalDatesExtractor.convert_to_date_time(date)
end
event.description = paragraph_with_date.children.reject{|c| c&.attributes.dig('class')&.value == 'discourse-local-date'}&.map{|c| c.text}&.join&.gsub(' → ', '')
event.post = @post
events << event
end

events
end


private

def self.convert_to_date_time(value)
return if value.blank?

attrs = value.attributes

datetime = attrs['data-date'].value
datetime << " #{attrs['data-time'].value}" if attrs['time']
timezone = attrs['data-timezone'].value || 'UTC'

ActiveSupport::TimeZone[timezone].parse(datetime)
end
end
15 changes: 15 additions & 0 deletions plugin.rb
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ def valid_event
# DISCOURSE CALENDAR

%w[
../app/controllers/discourse_calendar_controller.rb
../app/models/calendar_event.rb
../app/serializers/user_timezone_serializer.rb
../jobs/scheduled/create_holiday_events.rb
Expand All @@ -301,9 +302,23 @@ def valid_event
../lib/calendar.rb
../lib/event_validator.rb
../lib/group_timezones.rb
../lib/local_dates_extractor.rb
../lib/time_sniffer.rb
].each { |path| load File.expand_path(path, __FILE__) }

Discourse::Application.routes.append do
mount ::DiscourseCalendar::Engine, at: '/'
end

DiscourseCalendar::Engine.routes.draw do
get '/calendar/topic-calendar/:id' => 'discourse_calendar#topic_calendar',
constraints: { format: /ics/ }
get '/calendar/topic/:id' => 'discourse_calendar#topic_dates',
constraints: { format: /ics/ }
get '/calendar/post/:id' => 'discourse_calendar#post_dates',
constraints: { format: /ics/ }
end

register_post_custom_field_type(
DiscourseCalendar::CALENDAR_CUSTOM_FIELD,
:string
Expand Down