FEATURE: Display calendar events adjusted for timezones (#432)

Adds the option to enable a timezone adjustment for calendar events.
This will make it so events render offset from the grid to reflect the
appropriate start and end moments according to the viewer's timezone.
This commit is contained in:
Jan Cernik 2023-09-11 11:42:18 -03:00 committed by GitHub
parent 776cc411cd
commit 3fa341639b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 664 additions and 66 deletions

View File

@ -236,12 +236,18 @@ function initializeDiscourseCalendar(api) {
calendar.render();
_setStaticCalendarEvents(calendar, $calendar, post);
} else {
_setDynamicCalendarEvents(calendar, post, fullDay);
_setDynamicCalendarEvents(calendar, post, fullDay, timezone);
calendar.render();
_setDynamicCalendarOptions(calendar, $calendar);
}
_setupTimezonePicker(calendar, timezone);
const resetDynamicEvents = () => {
const selectedTimezone = calendar.getOption("timeZone");
calendar.getEvents().forEach((event) => event.remove());
_setDynamicCalendarEvents(calendar, post, fullDay, selectedTimezone);
};
_setupTimezonePicker(calendar, timezone, resetDynamicEvents);
}
function attachCalendar($elem, helper) {
@ -293,6 +299,7 @@ function initializeDiscourseCalendar(api) {
center: "title",
right: "month,basicWeek,listNextYear",
},
eventOrder: ["start", _orderByTz, "-duration", "allDay", "title"],
datesRender: (info) => {
if (showAddToCalendar) {
_insertAddToCalendarLinks(info);
@ -300,9 +307,24 @@ function initializeDiscourseCalendar(api) {
$calendarTitle.innerText = info.view.title;
},
eventPositioned: (info) => {
_setTimezoneOffset(info);
},
});
}
function _orderByTz(a, b) {
if (!siteSettings.enable_timezone_offset_for_calendar_events) {
return 0;
}
const offsetA = a.extendedProps.timezoneOffset;
const offsetB = b.extendedProps.timezoneOffset;
return offsetA === offsetB ? 0 : offsetA < offsetB ? -1 : 1;
}
function _convertHtmlToDate(html) {
const date = html.attr("data-date");
@ -325,19 +347,26 @@ function initializeDiscourseCalendar(api) {
function _buildEventObject(from, to) {
const hasTimeSpecified = (d) => {
if (!d) {
return false;
}
return d.hours() !== 0 || d.minutes() !== 0 || d.seconds() !== 0;
};
const hasTime =
hasTimeSpecified(to?.dateTime) || hasTimeSpecified(from?.dateTime);
const dateFormat = hasTime ? "YYYY-MM-DD HH:mm:ss Z" : "YYYY-MM-DD";
let event = {
start: from.dateTime.toDate(),
start: from.dateTime.format(dateFormat),
allDay: false,
};
if (to) {
if (hasTimeSpecified(to.dateTime) || hasTimeSpecified(from.dateTime)) {
event.end = to.dateTime.toDate();
if (hasTime) {
event.end = to.dateTime.format(dateFormat);
} else {
event.end = to.dateTime.add(1, "days").toDate();
event.end = to.dateTime.add(1, "days").format(dateFormat);
event.allDay = true;
}
} else {
@ -478,6 +507,10 @@ function initializeDiscourseCalendar(api) {
event.classNames = ["holiday"];
}
if (detail.timezoneOffset) {
event.extendedProps.timezoneOffset = detail.timezoneOffset;
}
return event;
}
@ -514,50 +547,143 @@ function initializeDiscourseCalendar(api) {
calendar.addEvent(event);
}
function _addGroupedEvent(calendar, post, detail) {
let htmlContent = "";
let usernames = [];
let localEventNames = [];
function _addGroupedEvent(calendar, post, detail, fullDay, calendarTz) {
const groupedEventData =
siteSettings.enable_timezone_offset_for_calendar_events && fullDay
? _splitGroupEventByTimezone(detail, calendarTz)
: [detail];
Object.keys(detail.localEvents)
.sort()
.forEach((key) => {
const localEvent = detail.localEvents[key];
htmlContent += `<b>${key}</b>: ${localEvent.usernames
.sort()
.join(", ")}<br>`;
usernames = usernames.concat(localEvent.usernames);
localEventNames.push(key);
});
groupedEventData.forEach((eventData) => {
let htmlContent = "";
let users = [];
let localEventNames = [];
const event = _buildEvent(detail);
event.classNames = ["grouped-event"];
Object.keys(eventData.localEvents)
.sort()
.forEach((key) => {
const localEvent = eventData.localEvents[key];
htmlContent += `<b>${key}</b>: ${localEvent.users
.map((u) => u.username)
.sort()
.join(", ")}<br>`;
users = users.concat(localEvent.users);
localEventNames.push(key);
});
if (usernames.length > 2) {
event.title = `(${usernames.length}) ${localEventNames[0]}`;
} else if (usernames.length === 1) {
event.title = usernames[0];
} else {
event.title = isMobileView
? `(${usernames.length}) ${localEventNames[0]}`
: `(${usernames.length}) ` + usernames.join(", ");
}
const event = _buildEvent(eventData);
event.classNames = ["grouped-event"];
if (localEventNames.length > 1) {
event.extendedProps.htmlContent = htmlContent;
} else {
if (usernames.length > 1) {
if (users.length > 2) {
event.title = `(${users.length}) ${localEventNames[0]}`;
} else if (users.length === 1) {
event.title = users[0].username;
} else {
event.title = isMobileView
? `(${users.length}) ${localEventNames[0]}`
: `(${users.length}) ` + users.map((u) => u.username).join(", ");
}
if (localEventNames.length > 1) {
event.extendedProps.htmlContent = htmlContent;
} else {
event.extendedProps.htmlContent = localEventNames[0];
if (users.length > 1) {
event.extendedProps.htmlContent = htmlContent;
} else {
event.extendedProps.htmlContent = localEventNames[0];
}
}
}
calendar.addEvent(event);
calendar.addEvent(event);
});
}
function _setDynamicCalendarEvents(calendar, post, fullDay) {
function _splitGroupEventByTimezone(detail, calendarTz) {
const calendarUtcOffset = moment.tz(calendarTz).utcOffset();
let timezonesOffsets = [];
let splittedEvents = [];
Object.values(detail.localEvents).forEach((event) => {
event.users.forEach((user) => {
const userUtcOffset = moment.tz(user.timezone).utcOffset();
const timezoneOffset = (calendarUtcOffset - userUtcOffset) / 60;
user.timezoneOffset = timezoneOffset;
timezonesOffsets.push(timezoneOffset);
});
});
[...new Set(timezonesOffsets)].forEach((offset, i) => {
let filteredLocalEvents = {};
let eventTimezones = [];
Object.keys(detail.localEvents).forEach((key) => {
const threshold =
siteSettings.split_grouped_events_by_timezone_threshold;
const filtered = detail.localEvents[key].users.filter(
(u) =>
Math.abs(u.timezoneOffset - (offset + threshold * i)) <= threshold
);
if (filtered.length > 0) {
filteredLocalEvents[key] = {
users: filtered,
};
filtered.forEach((u) => {
detail.localEvents[key].users.splice(
detail.localEvents[key].users.findIndex(
(e) => e.username === u.username
),
1
);
if (
!eventTimezones.find((t) => t.timezoneOffset === u.timezoneOffset)
) {
eventTimezones.push({
timezone: u.timezone,
timezoneOffset: u.timezoneOffset,
});
}
});
}
});
if (Object.keys(filteredLocalEvents).length > 0) {
const eventTimezone = _findAverageTimezone(eventTimezones);
let from = moment.tz(detail.from, eventTimezone.timezone);
let to = moment.tz(detail.to, eventTimezone.timezone);
_modifyDatesForTimezoneOffset(from, to, eventTimezone.timezoneOffset);
splittedEvents.push({
timezoneOffset: eventTimezone.timezoneOffset,
localEvents: filteredLocalEvents,
from: from.format("YYYY-MM-DD"),
to: to.format("YYYY-MM-DD"),
});
}
});
return splittedEvents;
}
function _findAverageTimezone(eventTimezones) {
const totalOffsets = eventTimezones.reduce(
(sum, timezone) => sum + timezone.timezoneOffset,
0
);
const averageOffset = totalOffsets / eventTimezones.length;
return eventTimezones.reduce((closest, timezone) => {
const difference = Math.abs(timezone.timezoneOffset - averageOffset);
return difference < Math.abs(closest.timezoneOffset - averageOffset)
? timezone
: closest;
});
}
function _setDynamicCalendarEvents(calendar, post, fullDay, calendarTz) {
const groupedEvents = [];
const calendarUtcOffset = moment.tz(calendarTz).utcOffset();
(post.calendar_details || []).forEach((detail) => {
switch (detail.type) {
@ -566,14 +692,24 @@ function initializeDiscourseCalendar(api) {
break;
case "standalone":
if (fullDay && detail.timezone) {
detail.from = moment
.tz(detail.from, detail.timezone)
.format("YYYY-MM-DD");
detail.to = moment
.tz(detail.to, detail.timezone)
.format("YYYY-MM-DD");
const eventDetail = { ...detail };
let from = moment.tz(detail.from, detail.timezone);
let to = moment.tz(detail.to, detail.timezone);
if (siteSettings.enable_timezone_offset_for_calendar_events) {
const eventUtcOffset = moment.tz(detail.timezone).utcOffset();
const timezoneOffset = (calendarUtcOffset - eventUtcOffset) / 60;
eventDetail.timezoneOffset = timezoneOffset;
_modifyDatesForTimezoneOffset(from, to, timezoneOffset);
}
eventDetail.from = from.format("YYYY-MM-DD");
eventDetail.to = to.format("YYYY-MM-DD");
_addStandaloneEvent(calendar, post, eventDetail);
} else {
_addStandaloneEvent(calendar, post, detail);
}
_addStandaloneEvent(calendar, post, detail);
break;
}
});
@ -601,24 +737,44 @@ function initializeDiscourseCalendar(api) {
formattedGroupedEvents[identifier].localEvents[groupedEvent.name] =
formattedGroupedEvents[identifier].localEvents[groupedEvent.name] || {
usernames: [],
users: [],
};
formattedGroupedEvents[identifier].localEvents[
groupedEvent.name
].usernames.push.apply(
formattedGroupedEvents[identifier].localEvents[groupedEvent.name]
.usernames,
groupedEvent.usernames
].users.push.apply(
formattedGroupedEvents[identifier].localEvents[groupedEvent.name].users,
groupedEvent.users
);
});
Object.keys(formattedGroupedEvents).forEach((key) => {
const formattedGroupedEvent = formattedGroupedEvents[key];
_addGroupedEvent(calendar, post, formattedGroupedEvent);
_addGroupedEvent(
calendar,
post,
formattedGroupedEvent,
fullDay,
calendarTz
);
});
}
function _modifyDatesForTimezoneOffset(from, to, timezoneOffset) {
if (timezoneOffset > 0) {
if (to.isValid()) {
to.add(1, "day");
} else {
to = from.clone().add(1, "day");
}
} else if (timezoneOffset < 0) {
if (!to.isValid()) {
to = from.clone();
}
from.subtract(1, "day");
}
}
function _getTimeZone($calendar, currentUser) {
let defaultTimezone = $calendar.attr("data-calendar-default-timezone");
const isValidDefaultTimezone = !!moment.tz.zone(defaultTimezone);
@ -629,19 +785,23 @@ function initializeDiscourseCalendar(api) {
return defaultTimezone || currentUser?.timezone || moment.tz.guess();
}
function _setupTimezonePicker(calendar, timezone) {
function _setupTimezonePicker(calendar, timezone, resetDynamicEvents) {
const tzPicker = document.querySelector(
".discourse-calendar-timezone-picker"
);
if (tzPicker) {
tzPicker.addEventListener("change", function (event) {
calendar.setOption("timeZone", event.target.value);
resetDynamicEvents();
_insertAddToCalendarLinks(calendar);
});
moment.tz.names().forEach((tz) => {
tzPicker.appendChild(new Option(tz, tz));
});
moment.tz
.names()
.filter((t) => !t.startsWith("Etc/GMT"))
.forEach((tz) => {
tzPicker.appendChild(new Option(tz, tz));
});
tzPicker.value = timezone;
} else {
@ -663,6 +823,63 @@ function initializeDiscourseCalendar(api) {
}
}
function _setTimezoneOffset(info) {
if (
!siteSettings.enable_timezone_offset_for_calendar_events ||
info.view.type === "listNextYear"
) {
return;
}
// The timezone offset works by calculating the hour difference
// between a target event and the calendar event. This is used to
// determine whether to add an extra day before or after the event.
// Then, it applies inline styling to resize the event to its
// original size while adjusting it to the respective timezone.
const timezoneOffset = info.event.extendedProps.timezoneOffset;
const segmentDuration = info.el.parentNode?.colSpan;
const basePctOffset = 100 / segmentDuration;
// Base margin required to shrink down the event by one day
const basePxOffset = 5.5 - segmentDuration;
// Default space between two consecutive events
// 5.5px = ( ( ( 2px margin + 3px padding ) * 2 ) + 1px border ) / 2
// K factors are used to adjust each side of the event based on the hour difference
// A '2' is added to the pxOffset to account for the default margin
if (timezoneOffset > 0) {
// When the event extends into the next day
if (info.isStart) {
const leftK = Math.abs(timezoneOffset) / 24;
const pctOffset = `${basePctOffset * leftK}%`;
const pxOffset = `${basePxOffset * leftK + 2}px`;
info.el.style.marginLeft = `calc(${pctOffset} + ${pxOffset})`;
}
if (info.isEnd) {
const rightK = (24 - Math.abs(timezoneOffset)) / 24;
const pctOffset = `${basePctOffset * rightK}%`;
const pxOffset = `${basePxOffset * rightK + 2}px`;
info.el.style.marginRight = `calc(${pctOffset} + ${pxOffset})`;
}
} else if (timezoneOffset < 0) {
// When the event starts on the previous day
if (info.isStart) {
const leftK = (24 - Math.abs(timezoneOffset)) / 24;
const pctOffset = `${basePctOffset * leftK}%`;
const pxOffset = `${basePxOffset * leftK + 2}px`;
info.el.style.marginLeft = `calc(${pctOffset} + ${pxOffset})`;
}
if (info.isEnd) {
const rightK = Math.abs(timezoneOffset) / 24;
const pctOffset = `${basePctOffset * rightK}%`;
const pxOffset = `${basePxOffset * rightK + 2}px`;
info.el.style.marginRight = `calc(${pctOffset} + ${pxOffset})`;
}
}
}
function _insertAddToCalendarLinkForEvent(event, eventSegmentDefMap) {
const eventTitle = event.eventRange.def.title;
let map = eventSegmentDefMap[event.eventRange.def.defId];

View File

@ -66,6 +66,10 @@
display: block;
}
.fc-event-container {
padding: 3px;
}
.fc-widget-header span {
padding: 3px 3px 3px 0.5em;
}

View File

@ -49,6 +49,14 @@ discourse_calendar:
sidebar_show_upcoming_events:
default: true
client: true
enable_timezone_offset_for_calendar_events:
default: false
client: true
hidden: true
split_grouped_events_by_timezone_threshold:
default: 0
client: true
hidden: true
discourse_post_event:
discourse_post_event_enabled:

View File

@ -363,22 +363,23 @@ after_initialize do
else
identifier = "#{event.region.split("_").first}-#{event.start_date.strftime("%j")}"
grouped[identifier] ||= {
type: :grouped,
from: event.start_date,
name: [],
usernames: [],
}
grouped[identifier] ||= { type: :grouped, from: event.start_date, name: [], users: [] }
user = User.find_by_username(event.username)
grouped[identifier][:name] << event.description
grouped[identifier][:usernames] << event.username
grouped[identifier][:users] << {
username: event.username,
timezone: user.present? ? user.user_option.timezone : nil,
}
end
end
grouped.each do |_, v|
v[:name].sort!.uniq!
v[:name] = v[:name].join(", ")
v[:usernames].sort!.uniq!
v[:users].sort! { |a, b| a[:username] <=> b[:username] }
v[:users].uniq! { |u| u[:username] }
end
standalones + grouped.values

View File

@ -35,9 +35,11 @@ describe PostSerializer do
it "groups calendar events correctly" do
user = Fabricate(:user)
user.upsert_custom_fields(::DiscourseCalendar::REGION_CUSTOM_FIELD => "ar")
user.user_option.update!(timezone: "America/Buenos_Aires")
user2 = Fabricate(:user)
user2.upsert_custom_fields(::DiscourseCalendar::REGION_CUSTOM_FIELD => "ar")
user2.user_option.update!(timezone: "America/Buenos_Aires")
post = create_post(raw: "[calendar]\n[/calendar]")
SiteSetting.holiday_calendar_topic_id = post.topic.id
@ -52,8 +54,11 @@ describe PostSerializer do
"Feriado puente turístico",
"Día de la Independencia",
)
expect(json[:post][:calendar_details].map { |x| x[:usernames] }).to all (
contain_exactly(user.username, user2.username)
expect(json[:post][:calendar_details].map { |x| x[:users] }).to all (
contain_exactly(
{ username: user.username, timezone: "America/Buenos_Aires" },
{ username: user2.username, timezone: "America/Buenos_Aires" },
)
)
end
end

View File

@ -0,0 +1,363 @@
import { acceptance } from "discourse/tests/helpers/qunit-helpers";
import { visit } from "@ember/test-helpers";
import { test } from "qunit";
const topicResponse = {
post_stream: {
posts: [
{
id: 375,
name: null,
username: "jan",
avatar_template: "/letter_avatar_proxy/v4/letter/j/ce7236/{size}.png",
created_at: "2023-09-08T16:50:07.638Z",
raw: '[calendar weekends=true tzPicker="true" fullDay="true" showAddToCalendar="false" defaultTimezone="Europe/Lisbon"] [/calendar]',
cooked:
'\u003cdiv class="discourse-calendar-wrap"\u003e\n\u003cdiv class="discourse-calendar-header"\u003e\n\u003ch2 class="discourse-calendar-title"\u003e\u003c/h2\u003e\n\u003cspan class="discourse-calendar-timezone-wrap"\u003e\n\u003cselect class="discourse-calendar-timezone-picker"\u003e\u003c/select\u003e\n\u003c/span\u003e\n\u003c/div\u003e\n\u003cdiv class="calendar" data-calendar-type="dynamic" data-calendar-default-timezone="Europe/Lisbon" data-weekends="true" data-calendar-show-add-to-calendar="false" data-calendar-full-day="true"\u003e\u003c/div\u003e\n\u003c/div\u003e',
post_number: 1,
post_type: 1,
updated_at: "2023-09-08T16:50:07.638Z",
reply_count: 0,
reply_to_post_number: null,
quote_count: 0,
incoming_link_count: 2,
reads: 1,
readers_count: 0,
score: 10.2,
yours: true,
topic_id: 252,
topic_slug: "awesome-calendar",
display_username: null,
primary_group_name: null,
flair_name: null,
flair_url: null,
flair_bg_color: null,
flair_color: null,
flair_group_id: null,
version: 1,
can_edit: true,
can_delete: false,
can_recover: false,
can_see_hidden_post: true,
can_wiki: true,
read: true,
user_title: null,
bookmarked: false,
actions_summary: [
{ id: 3, can_act: true },
{ id: 4, can_act: true },
{ id: 8, can_act: true },
{ id: 7, can_act: true },
],
moderator: false,
admin: true,
staff: true,
user_id: 1,
hidden: false,
trust_level: 1,
deleted_at: null,
user_deleted: false,
edit_reason: null,
can_view_edit_history: true,
wiki: false,
reviewable_id: 0,
reviewable_score_count: 0,
reviewable_score_pending_count: 0,
mentioned_users: [],
calendar_details: [
{
type: "standalone",
post_number: 2,
message: "Cordoba",
from: "2023-09-14T00:00:00.000Z",
to: "2023-09-14T00:00:00.000Z",
username: "jan",
recurring: null,
post_url: "/t/-/252/2",
timezone: "America/Cordoba",
},
{
type: "standalone",
post_number: 4,
message: "Moscow",
from: "2023-09-17T00:00:00.000Z",
to: "2023-09-18T00:00:00.000Z",
username: "jan",
recurring: null,
post_url: "/t/-/252/3",
timezone: "Europe/Moscow",
},
{
type: "standalone",
post_number: 3,
message: "Tokyo",
from: "2023-09-20T00:00:00.000Z",
to: "2023-09-21T00:00:00.000Z",
username: "jan",
recurring: null,
post_url: "/t/-/252/4",
timezone: "Asia/Tokyo",
},
{
type: "standalone",
post_number: 5,
message: "Lisbon",
from: "2023-09-28T00:00:00.000Z",
to: "2023-09-28T00:00:00.000Z",
username: "jan",
recurring: null,
post_url: "/t/-/252/5",
timezone: "Europe/Lisbon",
},
{
type: "grouped",
from: "2023-09-04T05:00:00.000Z",
name: "Labor Day",
users: [
{
username: "gmt-5_user",
timezone: "America/Chicago",
},
{
username: "gmt-6_user",
timezone: "America/Denver",
},
{
username: "gmt-7_user",
timezone: "America/Los_Angeles",
},
],
},
],
},
],
},
timeline_lookup: [[1, 0]],
tags: [],
tags_descriptions: {},
id: 252,
title: "Awesome Calendar",
fancy_title: "Awesome Calendar",
posts_count: 5,
created_at: "2023-09-08T16:50:07.371Z",
views: 1,
reply_count: 0,
like_count: 0,
last_posted_at: "2023-09-08T16:50:52.936Z",
visible: true,
closed: false,
archived: false,
has_summary: false,
archetype: "regular",
slug: "awesome-calendar",
category_id: 5,
word_count: 56,
deleted_at: null,
user_id: 1,
featured_link: null,
pinned_globally: false,
pinned_at: null,
pinned_until: null,
image_url: null,
slow_mode_seconds: 0,
draft: null,
draft_key: "topic_252",
draft_sequence: 9,
posted: true,
unpinned: null,
pinned: false,
current_post_number: 1,
highest_post_number: 4,
last_read_post_number: 4,
last_read_post_id: 378,
deleted_by: null,
has_deleted: false,
actions_summary: [
{ id: 4, count: 0, hidden: false, can_act: true },
{ id: 8, count: 0, hidden: false, can_act: true },
{ id: 7, count: 0, hidden: false, can_act: true },
],
chunk_size: 20,
bookmarked: false,
bookmarks: [],
topic_timer: null,
message_bus_last_id: 16,
participant_count: 1,
show_read_indicator: false,
thumbnails: null,
slow_mode_enabled_until: null,
summarizable: false,
details: {
can_edit: true,
notification_level: 3,
notifications_reason_id: 1,
can_move_posts: true,
can_delete: true,
can_remove_allowed_users: true,
can_invite_to: true,
can_invite_via_email: true,
can_create_post: true,
can_reply_as_new_topic: true,
can_flag_topic: true,
can_convert_topic: true,
can_review_topic: true,
can_close_topic: true,
can_archive_topic: true,
can_split_merge_topic: true,
can_edit_staff_notes: true,
can_toggle_topic_visibility: true,
can_pin_unpin_topic: true,
can_moderate_category: true,
can_remove_self_id: 1,
participants: [
{
id: 1,
username: "jan",
name: null,
avatar_template: "/letter_avatar_proxy/v4/letter/j/ce7236/{size}.png",
post_count: 4,
primary_group_name: null,
flair_name: null,
flair_url: null,
flair_color: null,
flair_bg_color: null,
flair_group_id: null,
admin: true,
trust_level: 1,
},
],
created_by: {
id: 1,
username: "jan",
name: null,
avatar_template: "/letter_avatar_proxy/v4/letter/j/ce7236/{size}.png",
},
last_poster: {
id: 1,
username: "jan",
name: null,
avatar_template: "/letter_avatar_proxy/v4/letter/j/ce7236/{size}.png",
},
},
};
function getEventByText(text) {
const events = [...document.querySelectorAll(".fc-day-grid-event")].filter(
(event) => event.textContent.includes(text)
);
return events.length === 1 ? events[0] : events;
}
function getRoundedPct(marginString) {
return Math.round(marginString.match(/(\d+(\.\d+)?)%/)[1]);
}
acceptance("Discourse Calendar - Timezone Offset", function (needs) {
needs.settings({
calendar_enabled: true,
enable_timezone_offset_for_calendar_events: true,
});
needs.pretender((server, helper) => {
server.get("/t/252.json", () => {
return helper.response(topicResponse);
});
});
test("doesn't apply an offset for events in the same timezone", async (assert) => {
await visit("/t/252");
const eventElement = getEventByText("Lisbon");
assert.notOk(eventElement.style.marginLeft);
assert.notOk(eventElement.style.marginRight);
});
test("applies the correct offset for events that extend into the next day", async (assert) => {
await visit("/t/252");
const eventElement = getEventByText("Cordoba");
assert.strictEqual(getRoundedPct(eventElement.style.marginLeft), 8); // ( ( 1 - (-3) ) / 24 ) * 50%
assert.strictEqual(getRoundedPct(eventElement.style.marginRight), 42); // ( ( 24 - ( 1 - (-3) ) ) / 24 ) * 50%
});
test("applies the correct offset for events that start on the previous day", async (assert) => {
await visit("/t/252");
const eventElement = getEventByText("Tokyo");
assert.strictEqual(getRoundedPct(eventElement.style.marginLeft), 22); // ( ( 24 - ( 9 - 1 ) ) / 24 ) * 33.33%
assert.strictEqual(getRoundedPct(eventElement.style.marginRight), 11); // ( ( 9 - 1 ) / 24 ) * 33.33%
});
test("applies the correct offset for multiline events", async (assert) => {
await visit("/t/252");
const eventElement = getEventByText("Moscow");
assert.strictEqual(getRoundedPct(eventElement[0].style.marginLeft), 46); // ( ( 24 - ( 1 - (-1) ) ) / 24 ) * 50%
assert.notOk(eventElement[0].style.marginRight);
assert.notOk(eventElement[1].style.marginLeft);
assert.strictEqual(getRoundedPct(eventElement[1].style.marginRight), 8); // ( ( 1 - (-1) ) / 24 ) * 100%
});
});
acceptance("Discourse Calendar - Splitted Grouped Events", function (needs) {
needs.settings({
calendar_enabled: true,
enable_timezone_offset_for_calendar_events: true,
split_grouped_events_by_timezone_threshold: 0,
});
needs.pretender((server, helper) => {
server.get("/t/252.json", () => {
return helper.response(topicResponse);
});
});
test("splits holidays events by timezone", async (assert) => {
await visit("/t/252");
const eventElement = document.querySelectorAll(
".fc-day-grid-event.grouped-event"
);
assert.ok(eventElement.length === 3);
assert.strictEqual(getRoundedPct(eventElement[0].style.marginLeft), 13); // ( ( 1 - (-5) ) / 24 ) * 50%
assert.strictEqual(getRoundedPct(eventElement[0].style.marginRight), 38); // ( ( 24 - ( 1 - (-5) ) ) / 24 ) * 50%
assert.strictEqual(getRoundedPct(eventElement[1].style.marginLeft), 15); // ( ( 1 - (-6) ) / 24 ) * 50%
assert.strictEqual(getRoundedPct(eventElement[1].style.marginRight), 35); // ( ( 24 - ( 1 - (-6) ) ) / 24 ) * 50%
assert.strictEqual(getRoundedPct(eventElement[2].style.marginLeft), 17); // ( ( 1 - (-7) ) / 24 ) * 50%
assert.strictEqual(getRoundedPct(eventElement[2].style.marginRight), 33); // ( ( 24 - ( 1 - (-7) ) ) / 24 ) * 50%
});
});
acceptance("Discourse Calendar - Grouped Events", function (needs) {
needs.settings({
calendar_enabled: true,
enable_timezone_offset_for_calendar_events: true,
split_grouped_events_by_timezone_threshold: 2,
});
needs.pretender((server, helper) => {
server.get("/t/252.json", () => {
return helper.response(topicResponse);
});
});
test("groups holidays events according to threshold", async (assert) => {
await visit("/t/252");
const eventElement = document.querySelectorAll(
".fc-day-grid-event.grouped-event"
);
assert.ok(eventElement.length === 1);
assert.strictEqual(getRoundedPct(eventElement[0].style.marginLeft), 15); // ( ( 1 - (-6) ) / 24 ) * 50%
assert.strictEqual(getRoundedPct(eventElement[0].style.marginRight), 35); // ( ( 24 - ( 1 - (-6) ) ) / 24 ) * 50%
});
});