mirror of
https://github.com/discourse/discourse.git
synced 2025-05-22 07:23:00 +08:00
FIX: improvements for download local dates (#14588)
* FIX: do not display add to calendar for past dates There is no value in saving past dates into calendar * FIX: remove postId and move ICS to frontend PostId is not necessary and will make the solution more generic for dates which doesn't belong to a specific post. Also, ICS file can be generated in JavaScript to avoid calling backend.
This commit is contained in:

committed by
GitHub

parent
ae0ca39bd1
commit
9062fd9b7a
@ -17,7 +17,7 @@ export default Controller.extend(ModalFunctionality, {
|
|||||||
this.currentUser.save(["default_calendar"]);
|
this.currentUser.save(["default_calendar"]);
|
||||||
}
|
}
|
||||||
if (this.selectedCalendar === "ics") {
|
if (this.selectedCalendar === "ics") {
|
||||||
downloadIcs(this.model.postId, this.model.title, this.model.dates);
|
downloadIcs(this.model.title, this.model.dates);
|
||||||
} else {
|
} else {
|
||||||
downloadGoogle(this.model.title, this.model.dates);
|
downloadGoogle(this.model.title, this.model.dates);
|
||||||
}
|
}
|
||||||
|
@ -2,17 +2,18 @@ import User from "discourse/models/user";
|
|||||||
import showModal from "discourse/lib/show-modal";
|
import showModal from "discourse/lib/show-modal";
|
||||||
import getURL from "discourse-common/lib/get-url";
|
import getURL from "discourse-common/lib/get-url";
|
||||||
|
|
||||||
export function downloadCalendar(postId, title, dates) {
|
export function downloadCalendar(title, dates) {
|
||||||
const currentUser = User.current();
|
const currentUser = User.current();
|
||||||
|
|
||||||
const formattedDates = formatDates(dates);
|
const formattedDates = formatDates(dates);
|
||||||
|
title = title.trim();
|
||||||
|
|
||||||
switch (currentUser.default_calendar) {
|
switch (currentUser.default_calendar) {
|
||||||
case "none_selected":
|
case "none_selected":
|
||||||
_displayModal(postId, title, formattedDates);
|
_displayModal(title, formattedDates);
|
||||||
break;
|
break;
|
||||||
case "ics":
|
case "ics":
|
||||||
downloadIcs(postId, title, formattedDates);
|
downloadIcs(title, formattedDates);
|
||||||
break;
|
break;
|
||||||
case "google":
|
case "google":
|
||||||
downloadGoogle(title, formattedDates);
|
downloadGoogle(title, formattedDates);
|
||||||
@ -20,17 +21,19 @@ export function downloadCalendar(postId, title, dates) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadIcs(postId, title, dates) {
|
export function downloadIcs(title, dates) {
|
||||||
let datesParam = "";
|
const REMOVE_FILE_AFTER = 20_000;
|
||||||
dates.forEach((date, index) => {
|
const file = new File([generateIcsData(title, dates)], {
|
||||||
datesParam = datesParam.concat(
|
type: "text/plain",
|
||||||
`&dates[${index}][starts_at]=${date.startsAt}&dates[${index}][ends_at]=${date.endsAt}`
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
const link = getURL(
|
|
||||||
`/calendars.ics?post_id=${postId}&title=${title}&${datesParam}`
|
const a = document.createElement("a");
|
||||||
);
|
document.body.appendChild(a);
|
||||||
window.open(link, "_blank", "noopener", "noreferrer");
|
a.style = "display: none";
|
||||||
|
a.href = window.URL.createObjectURL(file);
|
||||||
|
a.download = `${title.toLowerCase().replace(/[^\w]/g, "-")}.ics`;
|
||||||
|
a.click();
|
||||||
|
setTimeout(() => window.URL.revokeObjectURL(file), REMOVE_FILE_AFTER); //remove file to avoid memory leaks
|
||||||
}
|
}
|
||||||
|
|
||||||
export function downloadGoogle(title, dates) {
|
export function downloadGoogle(title, dates) {
|
||||||
@ -56,8 +59,28 @@ export function formatDates(dates) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function _displayModal(postId, title, dates) {
|
export function generateIcsData(title, dates) {
|
||||||
showModal("download-calendar", { model: { title, postId, dates } });
|
let data = "BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//Discourse//EN\n";
|
||||||
|
dates.forEach((date) => {
|
||||||
|
const startDate = moment(date.startsAt);
|
||||||
|
const endDate = moment(date.endsAt);
|
||||||
|
|
||||||
|
data = data.concat(
|
||||||
|
"BEGIN:VEVENT\n" +
|
||||||
|
`UID:${startDate.utc().format("x")}_${endDate.format("x")}\n` +
|
||||||
|
`DTSTAMP:${moment().utc().format("YMMDDTHHmmss")}Z\n` +
|
||||||
|
`DTSTART:${startDate.utc().format("YMMDDTHHmmss")}Z\n` +
|
||||||
|
`DTEND:${endDate.utc().format("YMMDDTHHmmss")}Z\n` +
|
||||||
|
`SUMMARY:${title}\n` +
|
||||||
|
"END:VEVENT\n"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
data = data.concat("END:VCALENDAR");
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
function _displayModal(title, dates) {
|
||||||
|
showModal("download-calendar", { model: { title, dates } });
|
||||||
}
|
}
|
||||||
|
|
||||||
function _formatDateForGoogleApi(date) {
|
function _formatDateForGoogleApi(date) {
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
import { module, test } from "qunit";
|
import { module, test } from "qunit";
|
||||||
import {
|
import {
|
||||||
downloadGoogle,
|
downloadGoogle,
|
||||||
downloadIcs,
|
|
||||||
formatDates,
|
formatDates,
|
||||||
|
generateIcsData,
|
||||||
} from "discourse/lib/download-calendar";
|
} from "discourse/lib/download-calendar";
|
||||||
import sinon from "sinon";
|
import sinon from "sinon";
|
||||||
|
|
||||||
@ -13,20 +13,28 @@ module("Unit | Utility | download-calendar", function (hooks) {
|
|||||||
sinon.stub(win, "focus");
|
sinon.stub(win, "focus");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("correct url for Ics", function (assert) {
|
test("correct data for Ics", function (assert) {
|
||||||
downloadIcs(1, "event", [
|
const data = generateIcsData("event test", [
|
||||||
{
|
{
|
||||||
startsAt: "2021-10-12T15:00:00.000Z",
|
startsAt: "2021-10-12T15:00:00.000Z",
|
||||||
endsAt: "2021-10-12T16:00:00.000Z",
|
endsAt: "2021-10-12T16:00:00.000Z",
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
window.open.calledWith(
|
data,
|
||||||
"/calendars.ics?post_id=1&title=event&&dates[0][starts_at]=2021-10-12T15:00:00.000Z&dates[0][ends_at]=2021-10-12T16:00:00.000Z",
|
`
|
||||||
"_blank",
|
BEGIN:VCALENDAR
|
||||||
"noopener",
|
VERSION:2.0
|
||||||
"noreferrer"
|
PRODID:-//Discourse//EN
|
||||||
)
|
BEGIN:VEVENT
|
||||||
|
UID:1634050800000_1634054400000
|
||||||
|
DTSTAMP:20213312T223320Z
|
||||||
|
DTSTART:20210012T150000Z
|
||||||
|
DTEND:20210012T160000Z
|
||||||
|
SUMMARY:event2
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -1,31 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
class CalendarsController < ApplicationController
|
|
||||||
skip_before_action :check_xhr, only: [ :index ], if: :ics_request?
|
|
||||||
requires_login
|
|
||||||
|
|
||||||
def download
|
|
||||||
@post = Post.find(calendar_params[:post_id])
|
|
||||||
@title = calendar_params[:title]
|
|
||||||
@dates = calendar_params[:dates].values
|
|
||||||
|
|
||||||
guardian.ensure_can_see!(@post)
|
|
||||||
|
|
||||||
respond_to do |format|
|
|
||||||
format.ics do
|
|
||||||
filename = "events-#{@title.parameterize}"
|
|
||||||
response.headers['Content-Disposition'] = "attachment; filename=\"#{filename}.#{request.format.symbol}\""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
|
||||||
|
|
||||||
def ics_request?
|
|
||||||
request.format.symbol == :ics
|
|
||||||
end
|
|
||||||
|
|
||||||
def calendar_params
|
|
||||||
params.permit(:post_id, :title, dates: [:starts_at, :ends_at])
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,15 +0,0 @@
|
|||||||
BEGIN:VCALENDAR
|
|
||||||
VERSION:2.0
|
|
||||||
PRODID:-//Discourse//<%= Discourse.current_hostname %>//<%= Discourse.full_version %>//EN
|
|
||||||
<% @dates.each do |date, index| %>
|
|
||||||
BEGIN:VEVENT
|
|
||||||
UID:post_#<%= @post.id %>_<%= date[:starts_at].to_datetime.to_i %>_<%= date[:ends_at].to_datetime.to_i %>@<%= Discourse.current_hostname %>
|
|
||||||
DTSTAMP:<%= Time.now.utc.strftime("%Y%m%dT%H%M%SZ") %>
|
|
||||||
DTSTART:<%= date[:starts_at].to_datetime.strftime("%Y%m%dT%H%M%SZ") %>
|
|
||||||
DTEND:<%= date[:ends_at].presence ? date[:ends_at].to_datetime.strftime("%Y%m%dT%H%M%SZ") : (date[:starts_at].to_datetime + 1.hour).strftime("%Y%m%dT%H%M%SZ") %>
|
|
||||||
SUMMARY:<%= @title %>
|
|
||||||
DESCRIPTION:<%= PrettyText.format_for_email(@post.excerpt, @post).html_safe %>
|
|
||||||
URL:<%= Discourse.base_url %>/t/-/<%= @post.topic_id %>/<%= @post.post_number %>
|
|
||||||
END:VEVENT
|
|
||||||
<% end %>
|
|
||||||
END:VCALENDAR
|
|
@ -650,8 +650,6 @@ Discourse::Application.routes.draw do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
get "/calendars" => "calendars#download", constraints: { format: :ics }
|
|
||||||
|
|
||||||
resources :bookmarks, only: %i[create destroy update] do
|
resources :bookmarks, only: %i[create destroy update] do
|
||||||
put "toggle_pin"
|
put "toggle_pin"
|
||||||
end
|
end
|
||||||
|
@ -172,54 +172,55 @@ function buildHtmlPreview(element, siteSettings) {
|
|||||||
previewsNode.classList.add("locale-dates-previews");
|
previewsNode.classList.add("locale-dates-previews");
|
||||||
htmlPreviews.forEach((htmlPreview) => previewsNode.appendChild(htmlPreview));
|
htmlPreviews.forEach((htmlPreview) => previewsNode.appendChild(htmlPreview));
|
||||||
|
|
||||||
previewsNode.appendChild(_downloadCalendarNode(element));
|
const calendarNode = _downloadCalendarNode(element);
|
||||||
|
if (calendarNode) {
|
||||||
|
previewsNode.appendChild(calendarNode);
|
||||||
|
}
|
||||||
|
|
||||||
return previewsNode.outerHTML;
|
return previewsNode.outerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function calculateStartAndEndDate(startDataset, endDataset) {
|
||||||
|
let startDate, endDate;
|
||||||
|
startDate = moment.tz(
|
||||||
|
`${startDataset.date} ${startDataset.time || ""}`.trim(),
|
||||||
|
startDataset.timezone
|
||||||
|
);
|
||||||
|
if (endDataset) {
|
||||||
|
endDate = moment.tz(
|
||||||
|
`${endDataset.date} ${endDataset.time || ""}`.trim(),
|
||||||
|
endDataset.timezone
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return [startDate, endDate];
|
||||||
|
}
|
||||||
|
|
||||||
function _downloadCalendarNode(element) {
|
function _downloadCalendarNode(element) {
|
||||||
|
const [startDataset, endDataset] = _rangeElements(element).map(
|
||||||
|
(dateElement) => dateElement.dataset
|
||||||
|
);
|
||||||
|
const [startDate, endDate] = calculateStartAndEndDate(
|
||||||
|
startDataset,
|
||||||
|
endDataset
|
||||||
|
);
|
||||||
|
|
||||||
|
if (startDate < moment().tz(startDataset.timezone)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const node = document.createElement("div");
|
const node = document.createElement("div");
|
||||||
node.classList.add("download-calendar");
|
node.classList.add("download-calendar");
|
||||||
node.innerHTML = `${renderIcon("string", "file")} ${I18n.t(
|
node.innerHTML = `${renderIcon("string", "file")} ${I18n.t(
|
||||||
"download_calendar.add_to_calendar"
|
"download_calendar.add_to_calendar"
|
||||||
)}`;
|
)}`;
|
||||||
const [startDataset, endDataset] = _rangeElements(element).map(
|
node.setAttribute("data-starts-at", startDate.toISOString());
|
||||||
(dateElement) => dateElement.dataset
|
|
||||||
);
|
|
||||||
node.setAttribute(
|
|
||||||
"data-starts-at",
|
|
||||||
moment
|
|
||||||
.tz(
|
|
||||||
`${startDataset.date} ${startDataset.time || ""}`.trim(),
|
|
||||||
startDataset.timezone
|
|
||||||
)
|
|
||||||
.toISOString()
|
|
||||||
);
|
|
||||||
if (endDataset) {
|
if (endDataset) {
|
||||||
node.setAttribute(
|
node.setAttribute("data-ends-at", endDate.toISOString());
|
||||||
"data-ends-at",
|
|
||||||
moment
|
|
||||||
.tz(
|
|
||||||
`${endDataset.date} ${endDataset.time || ""}`.trim(),
|
|
||||||
endDataset.timezone
|
|
||||||
)
|
|
||||||
.toISOString()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (!startDataset.time && !endDataset) {
|
if (!startDataset.time && !endDataset) {
|
||||||
node.setAttribute(
|
node.setAttribute("data-ends-at", startDate.add(24, "hours").toISOString());
|
||||||
"data-ends-at",
|
|
||||||
moment
|
|
||||||
.tz(`${startDataset.date}`, startDataset.timezone)
|
|
||||||
.add(24, "hours")
|
|
||||||
.toISOString()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
node.setAttribute("data-title", startDataset.title);
|
node.setAttribute("data-title", startDataset.title);
|
||||||
node.setAttribute(
|
|
||||||
"data-post-id",
|
|
||||||
element.closest("article")?.dataset?.postId
|
|
||||||
);
|
|
||||||
return node;
|
return node;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -260,7 +261,7 @@ export default {
|
|||||||
} else if (event?.target?.classList?.contains("download-calendar")) {
|
} else if (event?.target?.classList?.contains("download-calendar")) {
|
||||||
const dataset = event.target.dataset;
|
const dataset = event.target.dataset;
|
||||||
hidePopover(event);
|
hidePopover(event);
|
||||||
downloadCalendar(dataset.postId, dataset.title, [
|
downloadCalendar(dataset.title, [
|
||||||
{
|
{
|
||||||
startsAt: dataset.startsAt,
|
startsAt: dataset.startsAt,
|
||||||
endsAt: dataset.endsAt,
|
endsAt: dataset.endsAt,
|
||||||
|
@ -16,6 +16,12 @@ acceptance(
|
|||||||
needs.settings({ discourse_local_dates_enabled: true });
|
needs.settings({ discourse_local_dates_enabled: true });
|
||||||
needs.pretender((server, helper) => {
|
needs.pretender((server, helper) => {
|
||||||
const response = { ...fixturesByUrl["/t/281.json"] };
|
const response = { ...fixturesByUrl["/t/281.json"] };
|
||||||
|
const startDate = moment
|
||||||
|
.tz("Africa/Cairo")
|
||||||
|
.add(1, "days")
|
||||||
|
.format("YYYY-MM-DD");
|
||||||
|
response.post_stream.posts[0].cooked = `<p><span data-date=\"${startDate}\" data-time=\"13:00:00\" class=\"discourse-local-date\" data-timezone=\"Africa/Cairo\" data-email-preview=\"${startDate}T11:00:00Z UTC\">${startDate}T11:00:00Z</span></p>`;
|
||||||
|
|
||||||
server.get("/t/281.json", () => helper.response(response));
|
server.get("/t/281.json", () => helper.response(response));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -33,6 +39,32 @@ acceptance(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
acceptance(
|
||||||
|
"Local Dates - Download calendar is not available for dates in the past",
|
||||||
|
function (needs) {
|
||||||
|
needs.user({ default_calendar: "none_selected" });
|
||||||
|
needs.settings({ discourse_local_dates_enabled: true });
|
||||||
|
needs.pretender((server, helper) => {
|
||||||
|
const response = { ...fixturesByUrl["/t/281.json"] };
|
||||||
|
const startDate = moment
|
||||||
|
.tz("Africa/Cairo")
|
||||||
|
.subtract(1, "days")
|
||||||
|
.format("YYYY-MM-DD");
|
||||||
|
|
||||||
|
response.post_stream.posts[0].cooked = `<p><span data-date=\"${startDate}\" data-time=\"13:00:00\" class=\"discourse-local-date\" data-timezone=\"Africa/Cairo\" data-email-preview=\"${startDate}T11:00:00Z UTC\">${startDate}T11:00:00Z</span></p>`;
|
||||||
|
|
||||||
|
server.get("/t/281.json", () => helper.response(response));
|
||||||
|
});
|
||||||
|
|
||||||
|
test("Does not show add to calendar button", async function (assert) {
|
||||||
|
await visit("/t/local-dates/281");
|
||||||
|
|
||||||
|
await click(".discourse-local-date");
|
||||||
|
assert.ok(!exists(document.querySelector(".download-calendar")));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
acceptance(
|
acceptance(
|
||||||
"Local Dates - Download calendar with default calendar option set",
|
"Local Dates - Download calendar with default calendar option set",
|
||||||
function (needs) {
|
function (needs) {
|
||||||
@ -40,6 +72,12 @@ acceptance(
|
|||||||
needs.settings({ discourse_local_dates_enabled: true });
|
needs.settings({ discourse_local_dates_enabled: true });
|
||||||
needs.pretender((server, helper) => {
|
needs.pretender((server, helper) => {
|
||||||
const response = { ...fixturesByUrl["/t/281.json"] };
|
const response = { ...fixturesByUrl["/t/281.json"] };
|
||||||
|
const startDate = moment
|
||||||
|
.tz("Africa/Cairo")
|
||||||
|
.add(1, "days")
|
||||||
|
.format("YYYY-MM-DD");
|
||||||
|
response.post_stream.posts[0].cooked = `<p><span data-date=\"${startDate}\" data-time=\"13:00:00\" class=\"discourse-local-date\" data-timezone=\"Africa/Cairo\" data-email-preview=\"${startDate}T11:00:00Z UTC\">${startDate}T11:00:00Z</span></p>`;
|
||||||
|
response.title = " title to trim ";
|
||||||
server.get("/t/281.json", () => helper.response(response));
|
server.get("/t/281.json", () => helper.response(response));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -50,6 +88,10 @@ acceptance(
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("saves into default calendar", async function (assert) {
|
test("saves into default calendar", async function (assert) {
|
||||||
|
const startDate = moment
|
||||||
|
.tz("Africa/Cairo")
|
||||||
|
.add(1, "days")
|
||||||
|
.format("YYYYMMDD");
|
||||||
await visit("/t/local-dates/281");
|
await visit("/t/local-dates/281");
|
||||||
|
|
||||||
await click(".discourse-local-date");
|
await click(".discourse-local-date");
|
||||||
@ -57,7 +99,7 @@ acceptance(
|
|||||||
assert.ok(!exists(document.querySelector("#discourse-modal-title")));
|
assert.ok(!exists(document.querySelector("#discourse-modal-title")));
|
||||||
assert.ok(
|
assert.ok(
|
||||||
window.open.calledWith(
|
window.open.calledWith(
|
||||||
"https://www.google.com/calendar/event?action=TEMPLATE&text=Local%20dates&dates=20210930T110000Z/20210930T120000Z",
|
`https://www.google.com/calendar/event?action=TEMPLATE&text=title%20to%20trim&dates=${startDate}T110000Z/${startDate}T120000Z`,
|
||||||
"_blank",
|
"_blank",
|
||||||
"noopener",
|
"noopener",
|
||||||
"noreferrer"
|
"noreferrer"
|
||||||
|
@ -1,53 +0,0 @@
|
|||||||
# frozen_string_literal: true
|
|
||||||
|
|
||||||
require 'rails_helper'
|
|
||||||
|
|
||||||
describe CalendarsController do
|
|
||||||
fab!(:user) { Fabricate(:user) }
|
|
||||||
fab!(:post) { Fabricate(:post) }
|
|
||||||
|
|
||||||
describe "#download" do
|
|
||||||
it "returns an .ics file for dates" do
|
|
||||||
sign_in(user)
|
|
||||||
get "/calendars.ics", params: {
|
|
||||||
post_id: post.id,
|
|
||||||
title: "event title",
|
|
||||||
dates: {
|
|
||||||
"0": {
|
|
||||||
starts_at: "2021-10-12T15:00:00.000Z",
|
|
||||||
ends_at: "2021-10-13T16:30:00.000Z",
|
|
||||||
},
|
|
||||||
"1": {
|
|
||||||
starts_at: "2021-10-15T17:00:00.000Z",
|
|
||||||
ends_at: "2021-10-15T18:00:00.000Z",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expect(response.status).to eq(200)
|
|
||||||
expect(response.body).to eq(<<~ICS)
|
|
||||||
BEGIN:VCALENDAR
|
|
||||||
VERSION:2.0
|
|
||||||
PRODID:-//Discourse//#{Discourse.current_hostname}//#{Discourse.full_version}//EN
|
|
||||||
BEGIN:VEVENT
|
|
||||||
UID:post_##{post.id}_#{"2021-10-12T15:00:00.000Z".to_datetime.to_i}_#{"2021-10-13T16:30:00.000Z".to_datetime.to_i}@#{Discourse.current_hostname}
|
|
||||||
DTSTAMP:#{Time.now.utc.strftime("%Y%m%dT%H%M%SZ")}
|
|
||||||
DTSTART:#{"2021-10-12T15:00:00.000Z".to_datetime.strftime("%Y%m%dT%H%M%SZ")}
|
|
||||||
DTEND:#{"2021-10-13T16:30:00.000Z".to_datetime.strftime("%Y%m%dT%H%M%SZ")}
|
|
||||||
SUMMARY:event title
|
|
||||||
DESCRIPTION:Hello world
|
|
||||||
URL:#{Discourse.base_url}/t/-/#{post.topic_id}/#{post.post_number}
|
|
||||||
END:VEVENT
|
|
||||||
BEGIN:VEVENT
|
|
||||||
UID:post_##{post.id}_#{"2021-10-15T17:00:00.000Z".to_datetime.to_i}_#{"2021-10-15T18:00:00.000Z".to_datetime.to_i}@#{Discourse.current_hostname}
|
|
||||||
DTSTAMP:#{Time.now.utc.strftime("%Y%m%dT%H%M%SZ")}
|
|
||||||
DTSTART:#{"2021-10-15T17:00:00.000Z".to_datetime.strftime("%Y%m%dT%H%M%SZ")}
|
|
||||||
DTEND:#{"2021-10-15T18:00:00.000Z".to_datetime.strftime("%Y%m%dT%H%M%SZ")}
|
|
||||||
SUMMARY:event title
|
|
||||||
DESCRIPTION:Hello world
|
|
||||||
URL:#{Discourse.base_url}/t/-/#{post.topic_id}/#{post.post_number}
|
|
||||||
END:VEVENT
|
|
||||||
END:VCALENDAR
|
|
||||||
ICS
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
Reference in New Issue
Block a user