Site setting for all-day event times (#13)

This commit is contained in:
Mark VanLandingham 2020-01-16 14:14:57 -06:00 committed by GitHub
parent e735f87d5b
commit fc7e79a07d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 287 additions and 34 deletions

View File

@ -397,6 +397,7 @@ function initializeDiscourseCalendar(api) {
if ($timezonePicker.length) {
$timezonePicker.on("change", function(event) {
calendar.setOption("timeZone", event.target.value);
_insertAddToCalendarLinks(calendar);
});
moment.tz.names().forEach(timezone => {
@ -412,30 +413,63 @@ function initializeDiscourseCalendar(api) {
function _insertAddToCalendarLinks(info) {
if (info.view.type !== "listNextYear") return;
let eventSegments = info.view.eventRenderer.segs;
const eventSegments = info.view.eventRenderer.segs;
const eventSegmentDefMap = _eventSegmentDefMap(info);
for (const event of eventSegments) {
_insertAddToCalendarLinkForEvent(event);
_insertAddToCalendarLinkForEvent(event, eventSegmentDefMap);
}
}
function _insertAddToCalendarLinkForEvent(event) {
const container = event.el.querySelector(".fc-list-item-title");
const startDate = _formatDateForGoogleApi(event.start);
const endDate = _formatDateForGoogleApi(event.end);
function _insertAddToCalendarLinkForEvent(event, eventSegmentDefMap) {
const eventTitle = event.eventRange.def.title;
let map = eventSegmentDefMap[event.eventRange.def.defId];
let startDate = map.start;
let endDate = map.end;
endDate = endDate
? _formatDateForGoogleApi(endDate, event.eventRange.def.allDay)
: _endDateForAllDayEvent(startDate, event.eventRange.def.allDay);
startDate = _formatDateForGoogleApi(startDate, event.eventRange.def.allDay);
const link = document.createElement("a");
const title = I18n.t("discourse_calendar.add_to_calendar");
link.title = title;
link.appendChild(document.createTextNode(title));
link.href = `http://www.google.com/calendar/event?action=TEMPLATE&text=${encodeURIComponent(
container.childNodes[0].innerHTML
)}&dates=${startDate}/${endDate}`;
link.href = `
http://www.google.com/calendar/event?action=TEMPLATE&text=${encodeURIComponent(
eventTitle
)}&dates=${startDate}/${endDate}`;
link.target = "_blank";
link.classList.add("fc-list-item-add-to-calendar");
container.appendChild(link);
event.el.querySelector(".fc-list-item-title").appendChild(link);
}
function _formatDateForGoogleApi(date) {
return date.toISOString().replace(/-|:|\.\d\d\d/g, "");
function _formatDateForGoogleApi(date, allDay = false) {
if (!allDay) return date.toISOString().replace(/-|:|\.\d\d\d/g, "");
return moment(date)
.utc()
.format("YYYYMMDD");
}
function _endDateForAllDayEvent(startDate, allDay) {
const unit = allDay ? "days" : "hours";
return _formatDateForGoogleApi(
moment(startDate)
.add(1, unit)
.toDate(),
allDay
);
}
function _eventSegmentDefMap(info) {
let map = {};
for (let event of info.view.calendar.getEvents()) {
map[event._instance.defId] = { start: event.start, end: event.end };
}
return map;
}
}

View File

@ -3,6 +3,9 @@ en:
calendar_enabled: "Enable the discourse-calendar plugin. This will add support for a [calendar][/calendar] tag in the first post of a topic."
holiday_calendar_topic_id: "Topic ID of staffs holiday / absence calendar."
delete_expired_event_posts_after: "Posts with expired events will be automatically deleted after (n) hours. Set to -1 to disable deletion."
all_day_event_start_time: "Events that do not have a start time specified will start at this time. Format is HH:mm. For 6:00 am, enter 06:00"
all_day_event_end_time: "Events that do not have a end time specified will end at this time. Format is HH:mm. For 6:00 pm, enter 18:00"
all_day_event_time_error: "Invalid time. Format needs to be HH:mm (ex: 08:00)."
discourse_calendar:
calendar_must_be_in_first_post: "Calendar tag can only be used in first post of a topic."
more_than_one_calendar: "You cant have more than one calendar in a post."

View File

@ -8,3 +8,12 @@ plugins:
delete_expired_event_posts_after:
min: -1
default: 1
all_day_event_start_time:
default: ""
client: true
validator: "CalendarSettingsValidator"
all_day_event_end_time:
default: ""
client: true
validator: "CalendarSettingsValidator"

View File

@ -46,7 +46,6 @@ module Jobs
business_days = 1..5
load_until = 6.months.from_now
today = Date.today
holiday_hour = ::DiscourseCalendar::BEGINNING_OF_DAY_HOUR
users_in_region.keys.sort.each do |region|
holidays = Holidays.between(today, load_until, [region]).filter do |h|
@ -56,7 +55,8 @@ module Jobs
holidays.each do |next_holiday|
users_in_region[region].each do |user_id|
date = if tz = user_timezones[user_id]
datetime = next_holiday[:date].in_time_zone(tz).change(hour: holiday_hour)
datetime = next_holiday[:date].in_time_zone(tz)
datetime = datetime.change(holiday_hour_adjustment) if holiday_hour_adjustment
datetime.iso8601
else
next_holiday[:date].to_s
@ -73,5 +73,15 @@ module Jobs
op.save_custom_fields(true)
end
end
def holiday_hour_adjustment
@holiday_hour ||= begin
return false if SiteSetting.all_day_event_start_time.empty? || SiteSetting.all_day_event_end_time.empty?
{
hour: SiteSetting.all_day_event_start_time.split(':').first,
min: SiteSetting.all_day_event_start_time.split(':').second
}
end
end
end
end

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
class CalendarSettingsValidator
def initialize(opts = {})
@opts = opts
end
def valid_value?(val)
return true if val == ""
split = val.split(':')
return false if split.count != 2
hour = split.first
return false if hour.length != 2
return false unless hour.to_i >= 0 && hour.to_i < 24
minutes = split.second
return false if minutes.length != 2
return false unless minutes.to_i >= 0 && minutes.to_i < 60
true
end
def error_message
I18n.t('site_settings.all_day_event_time_error')
end
end

View File

@ -13,11 +13,15 @@ module DiscourseCalendar
end
from = self.convert_to_date_time(dates[0])
from = from.change(hour: DiscourseCalendar::BEGINNING_OF_DAY_HOUR) unless dates[0]['time']
to = self.convert_to_date_time(dates[1]) if dates.count == 2
if dates.count == 2
to = self.convert_to_date_time(dates[1])
to = to.change(hour: DiscourseCalendar::END_OF_DAY_HOUR) unless dates[1]['time']
unless SiteSetting.all_day_event_start_time.blank? || SiteSetting.all_day_event_end_time.blank?
if !dates[0]['time']
from = from.change(change_for_setting(SiteSetting.all_day_event_start_time))
end
if to && !dates[1]['time']
to = to.change(change_for_setting(SiteSetting.all_day_event_end_time))
end
end
html = post.cooked
@ -40,12 +44,16 @@ module DiscourseCalendar
def self.convert_to_date_time(value)
timezone = value["timezone"] || "UTC"
datetime = value['date'].to_s
datetime << " #{value['time']}" if value['time']
ActiveSupport::TimeZone[timezone].parse(datetime)
end
if value['time']
ActiveSupport::TimeZone[timezone].parse("#{value['date']} #{value['time']}")
else
ActiveSupport::TimeZone[timezone].parse(value['date'].to_s)
end
def self.change_for_setting(setting)
{
hour: setting.split(':').first,
min: setting.split(':').second
}
end
end
end

View File

@ -8,6 +8,10 @@
gem "holidays", "8.0.0", require: false
[
"../lib/calendar_settings_validator.rb",
].each { |path| load File.expand_path(path, __FILE__) }
enabled_site_setting :calendar_enabled
register_asset "stylesheets/vendor/fullcalendar.min.css"
@ -33,9 +37,6 @@ after_initialize do
USER_OPTIONS_TIMEZONE_ENABLED = UserOption.column_names.include?('timezone') rescue nil
BEGINNING_OF_DAY_HOUR = 6
END_OF_DAY_HOUR = 18
def self.users_on_holiday
PluginStore.get(PLUGIN_NAME, USERS_ON_HOLIDAY_KEY)
end
@ -67,6 +68,18 @@ after_initialize do
register_editable_user_custom_field(DiscourseCalendar::TIMEZONE_CUSTOM_FIELD)
end
DiscourseEvent.on(:site_setting_changed) do |name, old_value, new_value|
next unless [:all_day_event_start_time, :all_day_event_end_time].include? name
post_ids = PostCustomField.where(name: DiscourseCalendar::CALENDAR_DETAILS_CUSTOM_FIELD).pluck(:post_id)
Post.where(id: post_ids).each do |topic_post|
Post.where(topic_id: topic_post.topic_id).each do |post|
DiscourseCalendar::EventUpdater.update(post)
end
end
end
class DiscourseCalendar::Calendar
class << self
def extract(post)

View File

@ -71,9 +71,33 @@ describe DiscourseCalendar::CheckNextRegionalHolidays do
@op.reload
expect(@op.calendar_holidays[0]).to eq(
["fr", "Assomption", "2019-08-15T06:00:00+02:00", frenchy.username]
["fr", "Assomption", "2019-08-15T00:00:00+02:00", frenchy.username]
)
end
describe "with all day event start and end time" do
before do
SiteSetting.all_day_event_start_time = "06:00"
SiteSetting.all_day_event_end_time = "18:00"
end
it "uses the user TZ when available" do
frenchy = Fabricate(:user)
frenchy.custom_fields[DiscourseCalendar::REGION_CUSTOM_FIELD] = "fr"
frenchy.user_option.timezone = "Europe/Paris"
frenchy.user_option.save!
frenchy.save!
freeze_time Time.zone.local(2019, 8, 1)
subject.execute(nil)
@op.reload
expect(@op.calendar_holidays[0]).to eq(
["fr", "Assomption", "2019-08-15T06:00:00+02:00", frenchy.username]
)
end
end
end
context "when user_options.timezone column does NOT exist" do
@ -95,9 +119,32 @@ describe DiscourseCalendar::CheckNextRegionalHolidays do
@op.reload
expect(@op.calendar_holidays[0]).to eq(
["fr", "Assomption", "2019-08-15T06:00:00+02:00", frenchy.username]
["fr", "Assomption", "2019-08-15T00:00:00+02:00", frenchy.username]
)
end
describe "with all day event start and end time" do
before do
SiteSetting.all_day_event_start_time = "06:00"
SiteSetting.all_day_event_end_time = "18:00"
end
it "uses the users custom fields" do
frenchy = Fabricate(:user)
frenchy.custom_fields[DiscourseCalendar::REGION_CUSTOM_FIELD] = "fr"
frenchy.custom_fields[DiscourseCalendar::TIMEZONE_CUSTOM_FIELD] = "Europe/Paris"
frenchy.save!
freeze_time Time.zone.local(2019, 8, 1)
subject.execute(nil)
@op.reload
expect(@op.calendar_holidays[0]).to eq(
["fr", "Assomption", "2019-08-15T06:00:00+02:00", frenchy.username]
)
end
end
end
it "only takes into account active users" do

View File

@ -0,0 +1,28 @@
# frozen_string_literal: true
require "rails_helper"
describe CalendarSettingsValidator do
def expect_invalid(val)
expect(subject.valid_value?(val)).to eq(false)
end
def expect_valid(val)
expect(subject.valid_value?(val)).to eq(true)
end
it "only allows valid HH:mm formats" do
expect_invalid "markvanlan"
expect_invalid "000:00"
expect_invalid "00:000"
expect_invalid "24:00"
expect_invalid "00:60"
expect_invalid "002:30"
expect_valid ""
expect_valid "00:00"
expect_valid "23:00"
expect_valid "00:59"
expect_valid "23:59"
expect_valid "06:40"
end
end

View File

@ -21,7 +21,7 @@ describe "Dynamic calendar" do
op.reload
expect(op.calendar_details[p.post_number.to_s]).to eq([
"Rome", "2018-06-05T06:00:00+02:00", nil, p.user.username, nil
"Rome", "2018-06-05T00:00:00+02:00", nil, p.user.username, nil
])
end
@ -34,12 +34,13 @@ describe "Dynamic calendar" do
])
end
it "adds an entry with a range event" do
p = create_post(topic: op.topic, raw: 'Rome [date="2018-06-05" timezone="Europe/Paris"] → [date="2018-06-08" timezone="Europe/Paris"]')
op.reload
expect(op.calendar_details[p.post_number.to_s]).to eq([
"Rome", "2018-06-05T06:00:00+02:00", "2018-06-08T18:00:00+02:00", p.user.username, nil
"Rome", "2018-06-05T00:00:00+02:00", "2018-06-08T00:00:00+02:00", p.user.username, nil
])
end
@ -74,4 +75,38 @@ describe "Dynamic calendar" do
expect(op.calendar_details).to be_empty
end
describe "with all day event start and end time" do
before do
SiteSetting.all_day_event_start_time = "07:00"
SiteSetting.all_day_event_end_time = "18:00"
end
it "adds an entry with a single date event" do
p = create_post(topic: op.topic, raw: 'Rome [date="2018-06-05" timezone="Europe/Paris"]')
op.reload
expect(op.calendar_details[p.post_number.to_s]).to eq([
"Rome", "2018-06-05T07:00:00+02:00", nil, p.user.username, nil
])
end
it "adds an entry with a single date/time event" do
p = create_post(topic: op.topic, raw: 'Rome [date="2018-06-05" time="12:34:56"]')
op.reload
expect(op.calendar_details[p.post_number.to_s]).to eq([
"Rome", "2018-06-05T12:34:56Z", nil, p.user.username, nil
])
end
it "adds an entry with a range event" do
p = create_post(topic: op.topic, raw: 'Rome [date="2018-06-05" timezone="Europe/Paris"] → [date="2018-06-08" timezone="Europe/Paris"]')
op.reload
expect(op.calendar_details[p.post_number.to_s]).to eq([
"Rome", "2018-06-05T07:00:00+02:00", "2018-06-08T18:00:00+02:00", p.user.username, nil
])
end
end
end

View File

@ -3,7 +3,11 @@
require 'rails_helper'
describe DiscourseCalendar::EventUpdater do
before { SiteSetting.calendar_enabled = true }
before do
SiteSetting.calendar_enabled = true
SiteSetting.all_day_event_start_time = ""
SiteSetting.all_day_event_end_time = ""
end
it "will correctly update the associated first post calendar details" do
op = create_post(raw: "[calendar]\n[/calendar]")
@ -53,8 +57,8 @@ describe DiscourseCalendar::EventUpdater do
op.reload
_, from, to = op.calendar_details[post.post_number.to_s]
expect(from).to eq("2018-06-05T06:00:00Z")
expect(to).to eq("2018-06-11T18:00:00Z")
expect(from).to eq("2018-06-05T00:00:00Z")
expect(to).to eq("2018-06-11T00:00:00Z")
end
it "will work with timezone" do
@ -67,7 +71,7 @@ describe DiscourseCalendar::EventUpdater do
op.reload
_, from, to = op.calendar_details[post.post_number.to_s]
expect(from).to eq("2018-06-05T06:00:00+02:00")
expect(from).to eq("2018-06-05T00:00:00+02:00")
expect(to).to eq("2018-06-11T13:45:33-07:00")
end
@ -82,4 +86,38 @@ describe DiscourseCalendar::EventUpdater do
expect(post).to be_valid
end
describe "all day event site settings" do
before do
SiteSetting.all_day_event_start_time = "06:30"
SiteSetting.all_day_event_end_time = "18:00"
end
it "will work with no time date" do
op = create_post(raw: "[calendar]\n[/calendar]")
raw = %{Rome [date="2018-06-05"] [date="2018-06-11"]}
post = create_post(raw: raw, topic: op.topic)
CookedPostProcessor.new(post).post_process
op.reload
_, from, to = op.calendar_details[post.post_number.to_s]
expect(from).to eq("2018-06-05T06:30:00Z")
expect(to).to eq("2018-06-11T18:00:00Z")
end
it "will work with timezone" do
op = create_post(raw: "[calendar]\n[/calendar]")
raw = %{Rome [date="2018-06-05" timezone="Europe/Paris"] [date="2018-06-11" time="13:45:33" timezone="America/Los_Angeles"]}
post = create_post(raw: raw, topic: op.topic)
CookedPostProcessor.new(post).post_process
op.reload
_, from, to = op.calendar_details[post.post_number.to_s]
expect(from).to eq("2018-06-05T06:30:00+02:00")
expect(to).to eq("2018-06-11T13:45:33-07:00")
end
end
end