Allow for editing event titles:

- Add frontend for editing
- Save the original iCal properties when loading
- Use these to closely reproduce the original iCal for PUT'ing
This commit is contained in:
Sijmen 2021-07-05 21:50:58 +02:00
parent 024c8282b7
commit 3c2c33736b
Signed by: vijfhoek
GPG Key ID: DAF7821E067D9C48
6 changed files with 173 additions and 39 deletions

View File

@ -15,4 +15,10 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools.DotNet" Version="2.0.3" />
</ItemGroup>
<ItemGroup>
<None Update="Pages\Event.razor.scss">
<DependentUpon>Event.razor</DependentUpon>
</None>
</ItemGroup>
</Project>

View File

@ -3,24 +3,13 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using System.Xml;
namespace BlazorApp.Data
{
public class Event
{
public string Uid { get; set; }
public string Summary { get; set; }
public DateTime? DtStart { get; set; }
public DateTime? DtEnd { get; set; }
public string Description { get; set; }
public TimeSpan? Duration { get; set; }
public DateTime? CalculatedEnd => DtEnd ?? DtStart + Duration;
}
public class Calendar
{
public int Id { get; set; }
@ -127,17 +116,22 @@ namespace BlazorApp.Data
continue;
}
if (name == "END" && value == "VEVENT")
{
// End of the current event, add it to the list
events.Add(@event);
@event = null;
continue;
}
var parameters = left.Length == 2 ? left[1] : null;
// Add property to the RawProperties list.
// Used for rebuilding the full iCal for updating the event on the server.
@event.RawProperties.Add((name, parameters, value));
switch (name)
{
case "END":
if (value == "VEVENT")
{
// End of the current event, add it to the list
events.Add(@event);
@event = null;
}
break;
case "UID":
@event.Uid = value;
break;
@ -162,18 +156,8 @@ namespace BlazorApp.Data
return events;
}
/**
* <summary>Retrieves and parses all events from the CalDav server.</summary>
*/
public async Task<List<Event>> GetEvents()
private static List<Event> _filterSortEvents(List<Event> events)
{
var client = new HttpClient();
if (AuthorizationHeader != null)
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
var body = await client.GetStringAsync(Url);
var events = ParseICal(body);
var now = DateTime.Now;
events = events.FindAll(@event => @event.DtStart > now || @event.DtEnd > now);
@ -190,6 +174,26 @@ namespace BlazorApp.Data
return events;
}
/**
* <summary>Retrieves and parses all events from the CalDav server.</summary>
*/
public async Task<List<Event>> GetEvents()
{
var client = new HttpClient();
if (AuthorizationHeader != null)
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
var response = await client.GetAsync(Url);
var eTag = response.Headers.ETag?.Tag;
var body = await response.Content.ReadAsStringAsync();
var events = ParseICal(body);
foreach (var @event in events)
@event.ETag = eTag;
return _filterSortEvents(events);
}
/**
* <summary>Retrieves and parses an event with UID <code>uid</code> from the CalDav server.</summary>
*/
@ -199,6 +203,7 @@ namespace BlazorApp.Data
if (AuthorizationHeader != null)
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
// Request the event with UID `uid`.
var request = new HttpRequestMessage(new HttpMethod("REPORT"), Url);
request.Headers.Add("Depth", "1");
@ -225,13 +230,53 @@ namespace BlazorApp.Data
var result = await client.SendAsync(request);
var body = await result.Content.ReadAsStreamAsync();
// Parse the received XML
var reader = new XmlTextReader(body);
var document = new XmlDocument();
document.Load(reader);
var node = document.GetElementsByTagName("C:calendar-data")[0]?.InnerText;
var events = ParseICal(node);
return events.Count > 0 ? events[0] : null;
// Extract and parse the ICal data
var dataNode = document.GetElementsByTagName("C:calendar-data")[0]?.InnerText;
var events = ParseICal(dataNode);
if (events.Count == 0)
return null;
var @event = events[0];
// Extract the ETag and event collection href.
@event.ETag = document.GetElementsByTagName("getetag")[0]?.InnerText;
@event.Href = document.GetElementsByTagName("href")[0]?.InnerText;
return @event;
}
public async Task<Event> UpdateEvent(Event @event)
{
var client = new HttpClient();
if (AuthorizationHeader != null)
client.DefaultRequestHeaders.Add("Authorization", AuthorizationHeader);
// Setting If-Match header to the ETag is required when updating an event.
if (@event.ETag != null)
client.DefaultRequestHeaders.IfMatch.Add(new EntityTagHeaderValue(@event.ETag));
// Build a new URL based on the calendar's URL and the event's collection href.
var url = new Uri(new Uri(Url), @event.Href);
var content = new StringContent(@event.ToICal(), Encoding.UTF8, "text/calendar");
var response = await client.PutAsync(url, content);
// Parse the newly updated event.
var body = await response.Content.ReadAsStringAsync();
var events = ParseICal(body);
if (events.Count == 0)
return null;
var newEvent = events[0];
// Extract the updated ETag
newEvent.ETag = response.Headers.ETag?.Tag;
return newEvent;
}
}
}

59
BlazorApp/Data/Event.cs Normal file
View File

@ -0,0 +1,59 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace BlazorApp.Data
{
public class Event
{
public string Uid { get; set; }
public string Summary { get; set; }
public DateTime? DtStart { get; set; }
public DateTime? DtEnd { get; set; }
public string Description { get; set; }
public TimeSpan? Duration { get; set; }
public List<(string, string, string)> RawProperties { get; } = new();
public string ETag { get; set; }
public string Href { get; set; }
public DateTime? CalculatedEnd => DtEnd ?? DtStart + Duration;
private static string _escape(string s)
{
return s
.Replace(@"\", @"\\")
.Replace(",", @"\,")
.Replace(";", @"\;")
.Replace("\n", @"\n");
}
public string ToICal()
{
var sb = new StringBuilder();
sb.Append("BEGIN:VCALENDAR\r\n");
sb.Append("VERSION:2.0\r\n");
sb.Append("PRODID:-//Sijmen//blazor-calendar//EN\r\n");
sb.Append("BEGIN:VEVENT\r\n");
foreach (var (name, parameters, oldValue) in RawProperties)
{
sb.Append(name);
if (parameters != null)
sb.Append($";{parameters}");
var value = name switch
{
"SUMMARY" => _escape(Summary),
_ => oldValue
};
sb.Append($":{value}\r\n");
}
sb.Append("END:VEVENT\r\n");
sb.Append("END:VCALENDAR\r\n");
return sb.ToString();
}
}
}

View File

@ -13,7 +13,21 @@
}
else
{
<h1>@_event.Summary</h1>
@if (!_editTitle)
{
<h1 class="event-summary">
<span @onclick="() => _editTitle = true">@_event.Summary</span>
</h1>
}
else
{
<input @bind="@_event.Summary">
<button @onclick="UpdateEvent">Save</button>
}
<div>
<strong>ETag:</strong> @_event.ETag
</div>
@if (_event.DtStart != null)
{
@ -40,11 +54,18 @@ else
[Parameter]
public string EventUid { get; set; }
private Data.Calendar _calendar;
private Data.Event _event;
private bool _editTitle;
protected override async Task OnInitializedAsync()
{
var calendar = await Data.CalendarService.GetCalendarById(CalendarId);
_event = await calendar.GetEventByUid(EventUid);
_calendar = await Data.CalendarService.GetCalendarById(CalendarId);
_event = await _calendar.GetEventByUid(EventUid);
}
private async void UpdateEvent()
{
_event = await _calendar.UpdateEvent(_event);
}
}

View File

@ -0,0 +1,3 @@
.event-summary > span:hover {
border: 1px solid rgba(0, 0, 0, 0.2);
}

View File

@ -18,7 +18,7 @@
"BlazorApp": {
"commandName": "Project",
"dotnetRunMessages": "true",
"launchBrowser": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"