implemented ical exports
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -38,9 +38,10 @@ public interface Appointment {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* represent this event as ical entry
|
* represent this event as ical entry
|
||||||
|
* @param calendarIdentifier used to generate a unique id. pass something that is unique, but stable over time, e.g. a hostname
|
||||||
* @return the ical string
|
* @return the ical string
|
||||||
*/
|
*/
|
||||||
String ical();
|
String ical(String calendarIdentifier);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID of the appointment – unique within this system
|
* ID of the appointment – unique within this system
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ import org.json.JSONObject;
|
|||||||
* @param latitude the latitude
|
* @param latitude the latitude
|
||||||
*/
|
*/
|
||||||
public record Coords(double longitude, double latitude) {
|
public record Coords(double longitude, double latitude) {
|
||||||
|
/**
|
||||||
|
* get a string representing this coords in the ICAL format, see <a href="https://datatracker.ietf.org/doc/html/rfc5545#section-3.8.1.6">iCalendar spec</a>
|
||||||
|
* @return the formatted coords
|
||||||
|
*/
|
||||||
|
public String icalFormat(){
|
||||||
|
return "%s;%s".formatted(latitude,longitude);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* create a JSON object containing lat and lon of this coordinate
|
||||||
|
* @return the json object
|
||||||
|
*/
|
||||||
public JSONObject json() {
|
public JSONObject json() {
|
||||||
return new JSONObject(Map.of("lon", longitude, "lat", latitude));
|
return new JSONObject(Map.of("lon", longitude, "lat", latitude));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
/* © SRSoftware 2024 */
|
/* © SRSoftware 2024 */
|
||||||
package de.srsoftware.cal;
|
package de.srsoftware.cal;
|
||||||
|
|
||||||
|
import static de.srsoftware.cal.Util.*;
|
||||||
import static de.srsoftware.tools.Optionals.nullable;
|
import static de.srsoftware.tools.Optionals.nullable;
|
||||||
|
|
||||||
import de.srsoftware.cal.api.Appointment;
|
import de.srsoftware.cal.api.Appointment;
|
||||||
@@ -124,10 +125,24 @@ public class BaseAppointment implements Appointment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String ical() { // TODO: implement
|
public String ical(String calendarIdentifier) { // TODO: implement
|
||||||
return "converting event (%s) to ical not implemented".formatted(title);
|
var sb = new StringBuilder();
|
||||||
|
sb.append(contentLine(BEGIN,VEVENT));
|
||||||
|
if (calendarIdentifier != null) sb.append(contentLine(UID,"%s@%s".formatted(id(),calendarIdentifier)));
|
||||||
|
sb.append(contentLine(DTSTART,start().format(ICAL_DATE_FORMAT).replace(" ","T")));
|
||||||
|
end().map(end -> contentLine(DTEND,end.format(ICAL_DATE_FORMAT).replace(" ","T"))).ifPresent(sb::append);
|
||||||
|
sb.append(contentLine(SUMMARY,title()));
|
||||||
|
sb.append(contentLine(DESCRIPTION,description()));
|
||||||
|
coords().map(Coords::icalFormat).map(geo -> contentLine(GEO,geo)).ifPresent(sb::append);
|
||||||
|
if (!location().isBlank()) sb.append(contentLine(LOCATION,location()));
|
||||||
|
for (var attachment : attachments()) sb.append(contentLine("ATTACH;FMTYPE=%s".formatted(attachment.mime()),attachment.url().toString()));
|
||||||
|
for (var link : links) sb.append(contentLine("ATTACH;TITLE="+paramText(link.desciption()),link.url().toString()));
|
||||||
|
sb.append(contentLine("CLASS","PUBLIC"));
|
||||||
|
sb.append(contentLine(END,VEVENT));
|
||||||
|
return sb.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public long id() {
|
public long id() {
|
||||||
return id;
|
return id;
|
||||||
|
|||||||
@@ -6,8 +6,51 @@ import static de.srsoftware.tools.Error.error;
|
|||||||
import de.srsoftware.cal.api.Coords;
|
import de.srsoftware.cal.api.Coords;
|
||||||
import de.srsoftware.tools.Payload;
|
import de.srsoftware.tools.Payload;
|
||||||
import de.srsoftware.tools.Result;
|
import de.srsoftware.tools.Result;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
public class Util {
|
public class Util {
|
||||||
|
public static final String BEGIN = "BEGIN";
|
||||||
|
public static final String END = "END";
|
||||||
|
public static final String DESCRIPTION = "DESCRIPTION";
|
||||||
|
public static final String DTEND = "DTEND";
|
||||||
|
public static final String DTSTAMP = "DTSTAMP";
|
||||||
|
public static final String DTSTART = "DTSTART";
|
||||||
|
public static final String GEO = "GEO";
|
||||||
|
public static final DateTimeFormatter ICAL_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyyMMdd HHmmss");
|
||||||
|
public static final String LOCATION = "LOCATION";
|
||||||
|
public static final String PRODID = "PRODID";
|
||||||
|
public static final String SUMMARY = "SUMMARY";
|
||||||
|
public static final String UID = "UID";
|
||||||
|
public static final String VERSION = "VERSION";
|
||||||
|
public static final String VEVENT = "VEVENT";
|
||||||
|
public static final String VCALENDAR = "VCALENDAR";
|
||||||
|
|
||||||
|
private Util(){}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* formats a content line as defined in <a href="https://datatracker.ietf.org/doc/html/rfc5545#section-3.1">iCalendar spec</a>
|
||||||
|
* @param key the content line key
|
||||||
|
* @param value the content line value
|
||||||
|
* @return content line formatted as described in the spec
|
||||||
|
*/
|
||||||
|
public static String contentLine(String key, String value){
|
||||||
|
var contentLine = "%s:%s".formatted(key,value).trim();
|
||||||
|
// escape line breaks
|
||||||
|
contentLine = contentLine.replace("\\n","\\\\n").replace("\r\n","\\n").replace("\r","\\n").replace("\n","\\n");
|
||||||
|
var lines = new ArrayList<String>();
|
||||||
|
while (contentLine.length()>70){
|
||||||
|
var pos = contentLine.lastIndexOf(" ",70);
|
||||||
|
if (pos < 10) pos = 70;
|
||||||
|
var dummy = contentLine.substring(0,pos);
|
||||||
|
lines.add(dummy);
|
||||||
|
contentLine = '\t'+contentLine.substring(pos);
|
||||||
|
}
|
||||||
|
lines.add(contentLine);
|
||||||
|
lines.add("");
|
||||||
|
return String.join("\r\n",lines);
|
||||||
|
}
|
||||||
|
|
||||||
public static Result<Coords> extractCoords(String coords) {
|
public static Result<Coords> extractCoords(String coords) {
|
||||||
if (coords == null) return error("Argument is null");
|
if (coords == null) return error("Argument is null");
|
||||||
if (coords.isBlank()) return error("Argument is blank");
|
if (coords.isBlank()) return error("Argument is blank");
|
||||||
@@ -21,4 +64,35 @@ public class Util {
|
|||||||
return error(nfe, "Failed to parse coords from %s", coords);
|
return error(nfe, "Failed to parse coords from %s", coords);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public static String paramText(String param) {
|
||||||
|
return param
|
||||||
|
.replace("\n","\\n")
|
||||||
|
.replace("\"","''")
|
||||||
|
.replace(";","/")
|
||||||
|
.replace(",","/")
|
||||||
|
.replace(":","/");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* wraps a text (list of vevents in a vcalendar, as described in th <a href="https://datatracker.ietf.org/doc/html/rfc5545#section-3.4">iCalendar spec</a>
|
||||||
|
* @param ical the vevents list
|
||||||
|
* @param prodId the producer id of this icalendar
|
||||||
|
* @return the completed ical string
|
||||||
|
*/
|
||||||
|
public static Result<String> wrapIcal(Result<String> ical, String prodId) {
|
||||||
|
if (ical instanceof Payload<String> payload){
|
||||||
|
var calendar = new StringBuilder();
|
||||||
|
calendar.append(contentLine(BEGIN,VCALENDAR));
|
||||||
|
calendar.append(contentLine(VERSION,"2.0"));
|
||||||
|
calendar.append(contentLine(PRODID,prodId));
|
||||||
|
calendar.append(payload.get());
|
||||||
|
calendar.append(contentLine(END,VCALENDAR));
|
||||||
|
return Payload.of(calendar.toString());
|
||||||
|
}
|
||||||
|
return ical;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/* © SRSoftware 2024 */
|
||||||
|
package de.srsoftware.cal;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
public class UtilTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testContentLine(){
|
||||||
|
var longText = """
|
||||||
|
This text block, spanning several long lines, should be distributed over several lines, when passing it to the contentLine function.
|
||||||
|
Let`s see what happens… New line breaks should be introduced at roughly every 70 characters,
|
||||||
|
while the existing line breaks should be converted to '\n' escapes.""";
|
||||||
|
var expected = """
|
||||||
|
Test:This text block, spanning several long lines, should be\r
|
||||||
|
\t distributed over several lines, when passing it to the contentLine\r
|
||||||
|
\t function.\\nLet`s see what happens… New line breaks should be\r
|
||||||
|
\t introduced at roughly every 70 characters,\\nwhile the existing line\r
|
||||||
|
\t breaks should be converted to '\\n' escapes.\r\n""";
|
||||||
|
var result = Util.contentLine("Test",longText);
|
||||||
|
assertEquals(expected,result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,7 +33,7 @@ public class MariaDB implements Database {
|
|||||||
private static final String URLS = "urls";
|
private static final String URLS = "urls";
|
||||||
private static final String APPOINTMENT_ATTACHMENTS = "appointment_attachments";
|
private static final String APPOINTMENT_ATTACHMENTS = "appointment_attachments";
|
||||||
private static final String TAGS = "tags";
|
private static final String TAGS = "tags";
|
||||||
private static Connection connection;
|
private Connection connection;
|
||||||
|
|
||||||
private MariaDB(Connection conn) throws SQLException {
|
private MariaDB(Connection conn) throws SQLException {
|
||||||
connection = conn;
|
connection = conn;
|
||||||
@@ -180,7 +180,7 @@ public class MariaDB implements Database {
|
|||||||
public Result<Appointment> loadEvent(long id) {
|
public Result<Appointment> loadEvent(long id) {
|
||||||
try {
|
try {
|
||||||
var rs = select(ALL).from(APPOINTMENTS).where(AID, equal(id)).exec(connection);
|
var rs = select(ALL).from(APPOINTMENTS).where(AID, equal(id)).exec(connection);
|
||||||
Result<Appointment> result = rs.next() ? createAppointmentOf(rs).map(MariaDB::loadExtra) : NotFound.of("Failed to find appointment with id %s", id);
|
Result<Appointment> result = rs.next() ? createAppointmentOf(rs).map(this::loadExtra) : NotFound.of("Failed to find appointment with id %s", id);
|
||||||
rs.close();
|
rs.close();
|
||||||
return result;
|
return result;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@@ -192,7 +192,7 @@ public class MariaDB implements Database {
|
|||||||
public Result<Appointment> loadEvent(String location, LocalDateTime start) {
|
public Result<Appointment> loadEvent(String location, LocalDateTime start) {
|
||||||
try {
|
try {
|
||||||
var rs = select(ALL).from(APPOINTMENTS).where(LOCATION, equal(location)).where(START, equal(Timestamp.valueOf(start))).exec(connection);
|
var rs = select(ALL).from(APPOINTMENTS).where(LOCATION, equal(location)).where(START, equal(Timestamp.valueOf(start))).exec(connection);
|
||||||
Result<Appointment> result = rs.next() ? createAppointmentOf(rs).map(MariaDB::loadExtra) : error("Failed to find appointment starting %s @ %s", start, location);
|
Result<Appointment> result = rs.next() ? createAppointmentOf(rs).map(this::loadExtra) : error("Failed to find appointment starting %s @ %s", start, location);
|
||||||
rs.close();
|
rs.close();
|
||||||
return result;
|
return result;
|
||||||
} catch (SQLException e) {
|
} catch (SQLException e) {
|
||||||
@@ -200,11 +200,11 @@ public class MariaDB implements Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Result<Appointment> loadExtra(Result<BaseAppointment> res) {
|
private Result<Appointment> loadExtra(Result<BaseAppointment> res) {
|
||||||
return loadTags(res).map(MariaDB::loadLinks).map(MariaDB::loadAttachments);
|
return loadTags(res).map(this::loadLinks).map(this::loadAttachments);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Result<BaseAppointment> loadTags(Result<BaseAppointment> res) {
|
private Result<BaseAppointment> loadTags(Result<BaseAppointment> res) {
|
||||||
if (res.optional().isEmpty()) return transform(res);
|
if (res.optional().isEmpty()) return transform(res);
|
||||||
BaseAppointment event = res.optional().get();
|
BaseAppointment event = res.optional().get();
|
||||||
var id = event.id();
|
var id = event.id();
|
||||||
@@ -218,7 +218,7 @@ public class MariaDB implements Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Result<BaseAppointment> loadLinks(Result<BaseAppointment> res) {
|
private Result<BaseAppointment> loadLinks(Result<BaseAppointment> res) {
|
||||||
if (res.optional().isEmpty()) return transform(res);
|
if (res.optional().isEmpty()) return transform(res);
|
||||||
BaseAppointment event = res.optional().get();
|
BaseAppointment event = res.optional().get();
|
||||||
var id = event.id();
|
var id = event.id();
|
||||||
@@ -241,7 +241,7 @@ public class MariaDB implements Database {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Result<Appointment> loadAttachments(Result<BaseAppointment> res) {
|
private Result<Appointment> loadAttachments(Result<BaseAppointment> res) {
|
||||||
if (res.optional().isEmpty()) return transform(res);
|
if (res.optional().isEmpty()) return transform(res);
|
||||||
BaseAppointment event = res.optional().get();
|
BaseAppointment event = res.optional().get();
|
||||||
var id = event.id();
|
var id = event.id();
|
||||||
|
|||||||
@@ -91,16 +91,21 @@ public class ApiEndpoint extends PathHandler {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean doGet(String path, HttpExchange ex) throws IOException {
|
public boolean doGet(String path, HttpExchange ex) throws IOException {
|
||||||
|
String hostname = "TODO"; // TODO
|
||||||
|
String prodId = "TODO";
|
||||||
return switch (path) {
|
return switch (path) {
|
||||||
case "/event" -> sendContent(ex,getEvent(ex).map(ApiEndpoint::toJson).map(ApiEndpoint::httpError));
|
case "/event" -> sendContent(ex,getEvent(ex).map(ApiEndpoint::toJson).map(ApiEndpoint::httpError));
|
||||||
case "/events/ical"-> sendContent(ex,eventList(ex).map(ApiEndpoint::toIcal).map(ApiEndpoint::httpError));
|
case "/event/ical"-> sendContent(ex,getEvent(ex).map(event -> toIcal(event,hostname)).map(ical -> Util.wrapIcal(ical,prodId)).map(ApiEndpoint::httpError));
|
||||||
|
case "/events/ical"-> sendContent(ex,eventList(ex).map(list -> toIcalList(list,hostname)).map(ical -> Util.wrapIcal(ical,prodId)).map(ApiEndpoint::httpError));
|
||||||
case "/events/json" -> sendContent(ex,eventList(ex).map(ApiEndpoint::toJsonList).map(ApiEndpoint::httpError));
|
case "/events/json" -> sendContent(ex,eventList(ex).map(ApiEndpoint::toJsonList).map(ApiEndpoint::httpError));
|
||||||
case "/tags" -> listTags(ex);
|
case "/tags" -> listTags(ex);
|
||||||
default -> unknownPath(ex, path);
|
default -> unknownPath(ex, path);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
|
|
||||||
|
@Override
|
||||||
public boolean doPatch(String path, HttpExchange ex) throws IOException {
|
public boolean doPatch(String path, HttpExchange ex) throws IOException {
|
||||||
if ("/event".equals(path)) return sendContent(ex, updateEvent(ex));
|
if ("/event".equals(path)) return sendContent(ex, updateEvent(ex));
|
||||||
return unknownPath(ex, path);
|
return unknownPath(ex, path);
|
||||||
@@ -260,10 +265,15 @@ public class ApiEndpoint extends PathHandler {
|
|||||||
return Payload.of(event);
|
return Payload.of(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Result<String> toIcal(Result<List<Appointment>> res) {
|
private static Result<String> toIcal(Result<Appointment> res, String hostname) {
|
||||||
|
var opt = res.optional();
|
||||||
|
return opt.isEmpty() ? transform(res) : Payload.of(opt.get().ical(hostname));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Result<String> toIcalList(Result<List<Appointment>> res, String hostname) {
|
||||||
var opt = res.optional();
|
var opt = res.optional();
|
||||||
if (opt.isEmpty()) return transform(res);
|
if (opt.isEmpty()) return transform(res);
|
||||||
var list = opt.get().stream().map(Appointment::ical).collect(Collectors.joining("\n"));
|
var list = opt.get().stream().map(event -> event.ical(hostname)).collect(Collectors.joining("\n"));
|
||||||
return Payload.of(list);
|
return Payload.of(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,13 @@
|
|||||||
</head>
|
</head>
|
||||||
<body class="event">
|
<body class="event">
|
||||||
<nav />
|
<nav />
|
||||||
<h1 id="title">Loading…</h1>
|
|
||||||
<span id="buttons">
|
<span id="buttons">
|
||||||
<button title="create a copy of this event" onclick="window.top.location.href = location.href.replace('/static/event','/static/edit').replace('id=','copy=')">⧉ copy</button>
|
<button title="create a copy of this event" onclick="window.top.location.href = location.href.replace('/static/event','/static/edit').replace('id=','copy=')">⧉ copy</button>
|
||||||
<button title="edit the details of this event" onclick="window.top.location.href = location.href.replace('/static/event','/static/edit')">✍ edit</button>
|
<button title="edit the details of this event" onclick="window.top.location.href = location.href.replace('/static/event','/static/edit')">✍ edit</button>
|
||||||
<button title="delete this event" onclick="confirmDelete()">🗑 delete</button>
|
<button title="delete this event" onclick="confirmDelete()">🗑 delete</button>
|
||||||
|
<button title="download this event" onclick="ical()"><u>⬇</u> download</button>
|
||||||
</span>
|
</span>
|
||||||
|
<h1 id="title">Loading…</h1>
|
||||||
<div id="time">Loading time…</div>
|
<div id="time">Loading time…</div>
|
||||||
<div id="description">Loading description…</div>
|
<div id="description">Loading description…</div>
|
||||||
<div id="links">Loading links…</div>
|
<div id="links">Loading links…</div>
|
||||||
|
|||||||
@@ -159,4 +159,8 @@ table#eventlist{
|
|||||||
}
|
}
|
||||||
.highlight{
|
.highlight{
|
||||||
background: wheat;
|
background: wheat;
|
||||||
|
}
|
||||||
|
|
||||||
|
.event h1{
|
||||||
|
margin-top: 40px;
|
||||||
}
|
}
|
||||||
@@ -20,10 +20,10 @@ async function handleEventData(response){
|
|||||||
if (response.ok){
|
if (response.ok){
|
||||||
var event = await response.json();
|
var event = await response.json();
|
||||||
document.getElementsByTagName('h1')[0].innerHTML = event.title ? event.title : '';
|
document.getElementsByTagName('h1')[0].innerHTML = event.title ? event.title : '';
|
||||||
document.getElementById('time').innerHTML = event.start + (event.end ? '…'+event.end : '');
|
element('time').innerHTML = '<b>'+event.start +'</b>'+ (event.end ? '…'+event.end : '') + ' @ '+event.location;
|
||||||
addDescription(event);
|
addDescription(event);
|
||||||
document.getElementById('tags').innerHTML = "Tags: "+event.tags.join(" ");
|
element('tags').innerHTML = "Tags: "+event.tags.join(" ");
|
||||||
var links = document.getElementById('links');
|
var links = element('links');
|
||||||
links.innerHTML = "";
|
links.innerHTML = "";
|
||||||
links.appendChild(linkList(event.links));
|
links.appendChild(linkList(event.links));
|
||||||
attachmentList(event.attachments);
|
attachmentList(event.attachments);
|
||||||
@@ -78,4 +78,15 @@ function confirmDelete(){
|
|||||||
}
|
}
|
||||||
}).then(handleDeleted);
|
}).then(handleDeleted);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ical(){
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
var id = urlParams.get('id');
|
||||||
|
if (id) {
|
||||||
|
var elem = create('a');
|
||||||
|
elem.setAttribute('href',location.href.replace('/static/event','/api/event/ical'));
|
||||||
|
elem.setAttribute('download',`event-${id}.ics`);
|
||||||
|
elem.click();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -115,19 +115,21 @@ function loadCurrentEvents(){
|
|||||||
function openIcal(){
|
function openIcal(){
|
||||||
var url = location.href;
|
var url = location.href;
|
||||||
var query = location.search;
|
var query = location.search;
|
||||||
|
var params = new URLSearchParams(query);
|
||||||
|
var tags = params.get('tags');
|
||||||
var pos = url.indexOf('?');
|
var pos = url.indexOf('?');
|
||||||
if (pos>1) url = url.substring(0,pos);
|
if (pos>1) url = url.substring(0,pos);
|
||||||
if (!url.endsWith('/')) url += '/';
|
if (!url.endsWith('/')) url += '/';
|
||||||
url += 'api/events/ical'+query;
|
url += 'api/events/ical'+query;
|
||||||
var elem = create('a');
|
var elem = create('a');
|
||||||
elem.setAttribute('href',url);
|
elem.setAttribute('href',url);
|
||||||
elem.setAttribute('download','calendar.ical');
|
elem.setAttribute('download',(tags?tags.replace(',','+'):'calendar')+'.ics');
|
||||||
elem.click();
|
elem.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
// shows the given url in an overlayed iframe
|
// shows the given url in an overlayed iframe
|
||||||
function showOverlay(url){
|
function showOverlay(url){
|
||||||
var div = document.getElementById('overlay');
|
var div = element('overlay');
|
||||||
div.innerHTML = '';
|
div.innerHTML = '';
|
||||||
var iframe = document.createElement('iframe');
|
var iframe = document.createElement('iframe');
|
||||||
iframe.src = url;
|
iframe.src = url;
|
||||||
|
|||||||
@@ -119,6 +119,42 @@ paths:
|
|||||||
description: server fault
|
description: server fault
|
||||||
summary:
|
summary:
|
||||||
store a new event in the database
|
store a new event in the database
|
||||||
|
/api/event/ical:
|
||||||
|
get:
|
||||||
|
parameters:
|
||||||
|
- description: the appointment id
|
||||||
|
in: query
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
example: 42
|
||||||
|
format: int64
|
||||||
|
type: number
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: successful operation
|
||||||
|
content:
|
||||||
|
text/calendar:
|
||||||
|
schema:
|
||||||
|
description: event in ical format
|
||||||
|
example: |-
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:OpenCloudCal
|
||||||
|
BEGIN:VEVENT
|
||||||
|
UID:42@cal.srsoftware.de
|
||||||
|
DTSTART:20241230T100757Z
|
||||||
|
DTEND:20241231T100757Z
|
||||||
|
SUMMARY:Demolition of the earth
|
||||||
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
|
type: string
|
||||||
|
'400':
|
||||||
|
description: invalid input
|
||||||
|
'500':
|
||||||
|
description: server fault
|
||||||
|
summary: get an event as ical file
|
||||||
|
|
||||||
/api/events/ical:
|
/api/events/ical:
|
||||||
get:
|
get:
|
||||||
description: |-
|
description: |-
|
||||||
|
|||||||
Reference in New Issue
Block a user