Outsource some Calendar info to CalendarMetadata struct

This commit is contained in:
Lennart
2025-08-24 12:52:28 +02:00
parent f3a1f27caf
commit a4285fb2ac
10 changed files with 84 additions and 62 deletions

View File

@@ -43,24 +43,24 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0") let mut ical_calendar_builder = IcalCalendarBuilder::version("4.0")
.gregorian() .gregorian()
.prodid("RustiCal"); .prodid("RustiCal");
if calendar.displayname.is_some() { if let Some(displayname) = calendar.meta.displayname {
ical_calendar_builder = ical_calendar_builder.set(Property { ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-CALNAME".to_owned(), name: "X-WR-CALNAME".to_owned(),
value: calendar.displayname, value: Some(displayname),
params: None, params: None,
}); });
} }
if calendar.description.is_some() { if let Some(description) = calendar.meta.description {
ical_calendar_builder = ical_calendar_builder.set(Property { ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-CALDESC".to_owned(), name: "X-WR-CALDESC".to_owned(),
value: calendar.description, value: Some(description),
params: None, params: None,
}); });
} }
if calendar.timezone_id.is_some() { if let Some(timezone_id) = calendar.timezone_id {
ical_calendar_builder = ical_calendar_builder.set(Property { ical_calendar_builder = ical_calendar_builder.set(Property {
name: "X-WR-TIMEZONE".to_owned(), name: "X-WR-TIMEZONE".to_owned(),
value: calendar.timezone_id, value: Some(timezone_id),
params: None, params: None,
}); });
} }

View File

@@ -10,7 +10,9 @@ use ical::{
parser::{Component, ComponentMut}, parser::{Component, ComponentMut},
}; };
use rustical_ical::{CalendarObject, CalendarObjectType}; use rustical_ical::{CalendarObject, CalendarObjectType};
use rustical_store::{Calendar, CalendarStore, SubscriptionStore, auth::Principal}; use rustical_store::{
Calendar, CalendarMetadata, CalendarStore, SubscriptionStore, auth::Principal,
};
use std::io::BufReader; use std::io::BufReader;
use tracing::instrument; use tracing::instrument;
@@ -83,10 +85,12 @@ pub async fn route_import<C: CalendarStore, S: SubscriptionStore>(
let new_cal = Calendar { let new_cal = Calendar {
principal, principal,
id: cal_id, id: cal_id,
displayname, meta: CalendarMetadata {
order: 0, displayname,
description, order: 0,
color: None, description,
color: None,
},
timezone_id, timezone_id,
deleted_at: None, deleted_at: None,
synctoken: 0, synctoken: 0,

View File

@@ -8,7 +8,7 @@ use ical::IcalParser;
use rustical_dav::xml::HrefElement; use rustical_dav::xml::HrefElement;
use rustical_ical::CalendarObjectType; use rustical_ical::CalendarObjectType;
use rustical_store::auth::Principal; use rustical_store::auth::Principal;
use rustical_store::{Calendar, CalendarStore, SubscriptionStore}; use rustical_store::{Calendar, CalendarMetadata, CalendarStore, SubscriptionStore};
use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag}; use rustical_xml::{Unparsed, XmlDeserialize, XmlDocument, XmlRootTag};
use tracing::instrument; use tracing::instrument;
@@ -112,11 +112,13 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
let calendar = Calendar { let calendar = Calendar {
id: cal_id.to_owned(), id: cal_id.to_owned(),
principal: principal.to_owned(), principal: principal.to_owned(),
order: request.calendar_order.unwrap_or(0), meta: CalendarMetadata {
displayname: request.displayname, order: request.calendar_order.unwrap_or(0),
displayname: request.displayname,
color: request.calendar_color,
description: request.calendar_description,
},
timezone_id, timezone_id,
color: request.calendar_color,
description: request.calendar_description,
deleted_at: None, deleted_at: None,
synctoken: 0, synctoken: 0,
subscription_url: request.source.map(|href| href.href), subscription_url: request.source.map(|href| href.href),

View File

@@ -128,10 +128,10 @@ impl Resource for CalendarResource {
Ok(match prop { Ok(match prop {
CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop { CalendarPropWrapperName::Calendar(prop) => CalendarPropWrapper::Calendar(match prop {
CalendarPropName::CalendarColor => { CalendarPropName::CalendarColor => {
CalendarProp::CalendarColor(self.cal.color.clone()) CalendarProp::CalendarColor(self.cal.meta.color.clone())
} }
CalendarPropName::CalendarDescription => { CalendarPropName::CalendarDescription => {
CalendarProp::CalendarDescription(self.cal.description.clone()) CalendarProp::CalendarDescription(self.cal.meta.description.clone())
} }
CalendarPropName::CalendarTimezone => { CalendarPropName::CalendarTimezone => {
CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| { CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| {
@@ -146,7 +146,7 @@ impl Resource for CalendarResource {
CalendarProp::CalendarTimezoneId(self.cal.timezone_id.clone()) CalendarProp::CalendarTimezoneId(self.cal.timezone_id.clone())
} }
CalendarPropName::CalendarOrder => { CalendarPropName::CalendarOrder => {
CalendarProp::CalendarOrder(Some(self.cal.order)) CalendarProp::CalendarOrder(Some(self.cal.meta.order))
} }
CalendarPropName::SupportedCalendarComponentSet => { CalendarPropName::SupportedCalendarComponentSet => {
CalendarProp::SupportedCalendarComponentSet(self.cal.components.clone().into()) CalendarProp::SupportedCalendarComponentSet(self.cal.components.clone().into())
@@ -187,11 +187,11 @@ impl Resource for CalendarResource {
match prop { match prop {
CalendarPropWrapper::Calendar(prop) => match prop { CalendarPropWrapper::Calendar(prop) => match prop {
CalendarProp::CalendarColor(color) => { CalendarProp::CalendarColor(color) => {
self.cal.color = color; self.cal.meta.color = color;
Ok(()) Ok(())
} }
CalendarProp::CalendarDescription(description) => { CalendarProp::CalendarDescription(description) => {
self.cal.description = description; self.cal.meta.description = description;
Ok(()) Ok(())
} }
CalendarProp::CalendarTimezone(timezone) => { CalendarProp::CalendarTimezone(timezone) => {
@@ -236,7 +236,7 @@ impl Resource for CalendarResource {
Ok(()) Ok(())
} }
CalendarProp::CalendarOrder(order) => { CalendarProp::CalendarOrder(order) => {
self.cal.order = order.unwrap_or_default(); self.cal.meta.order = order.unwrap_or_default();
Ok(()) Ok(())
} }
CalendarProp::SupportedCalendarComponentSet(comp_set) => { CalendarProp::SupportedCalendarComponentSet(comp_set) => {
@@ -264,11 +264,11 @@ impl Resource for CalendarResource {
match prop { match prop {
CalendarPropWrapperName::Calendar(prop) => match prop { CalendarPropWrapperName::Calendar(prop) => match prop {
CalendarPropName::CalendarColor => { CalendarPropName::CalendarColor => {
self.cal.color = None; self.cal.meta.color = None;
Ok(()) Ok(())
} }
CalendarPropName::CalendarDescription => { CalendarPropName::CalendarDescription => {
self.cal.description = None; self.cal.meta.description = None;
Ok(()) Ok(())
} }
CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => { CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => {
@@ -277,7 +277,7 @@ impl Resource for CalendarResource {
} }
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
CalendarPropName::CalendarOrder => { CalendarPropName::CalendarOrder => {
self.cal.order = 0; self.cal.meta.order = 0;
Ok(()) Ok(())
} }
CalendarPropName::SupportedCalendarComponentSet => { CalendarPropName::SupportedCalendarComponentSet => {
@@ -300,10 +300,10 @@ impl Resource for CalendarResource {
} }
fn get_displayname(&self) -> Option<&str> { fn get_displayname(&self) -> Option<&str> {
self.cal.displayname.as_deref() self.cal.meta.displayname.as_deref()
} }
fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> { fn set_displayname(&mut self, name: Option<String>) -> Result<(), rustical_dav::Error> {
self.cal.displayname = name; self.cal.meta.displayname = name;
Ok(()) Ok(())
} }

View File

@@ -1,13 +1,13 @@
<h2>{{ user.id }}'s Calendars</h2> <h2>{{ user.id }}'s Calendars</h2>
<ul class="collection-list"> <ul class="collection-list">
{% for (meta, calendar) in calendars %} {% for (meta, calendar) in calendars %}
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %} {% let color = calendar.meta.color.to_owned().unwrap_or("transparent".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}"> <li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"></a> <a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id }}"></a>
<div class="inner"> <div class="inner">
<span class="title"> <span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%} {%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }} {{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
<div class="comps"> <div class="comps">
{% for comp in calendar.components %} {% for comp in calendar.components %}
<span>{{ comp }}</span> <span>{{ comp }}</span>
@@ -15,7 +15,7 @@
</div> </div>
</span> </span>
<span class="description"> <span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %} {% if let Some(description) = calendar.meta.description %}{{ description }}{% endif %}
</span> </span>
{% if let Some(subscription_url) = calendar.subscription_url %} {% if let Some(subscription_url) = calendar.subscription_url %}
<span class="subscription-url">{{ subscription_url }}</span> <span class="subscription-url">{{ subscription_url }}</span>
@@ -29,9 +29,9 @@
principal="{{ calendar.principal }}" principal="{{ calendar.principal }}"
cal_id="{{ calendar.id }}" cal_id="{{ calendar.id }}"
timezone_id="{{ calendar.timezone_id.as_deref().unwrap_or_default() }}" timezone_id="{{ calendar.timezone_id.as_deref().unwrap_or_default() }}"
displayname="{{ calendar.displayname.as_deref().unwrap_or_default() }}" displayname="{{ calendar.meta.displayname.as_deref().unwrap_or_default() }}"
description="{{ calendar.description.as_deref().unwrap_or_default() }}" description="{{ calendar.meta.description.as_deref().unwrap_or_default() }}"
color="{{ calendar.color.as_deref().unwrap_or_default() }}" color="{{ calendar.meta.color.as_deref().unwrap_or_default() }}"
components="{{ calendar.components | json }}" components="{{ calendar.components | json }}"
></edit-calendar-form> ></edit-calendar-form>
<delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button> <delete-button trash href="/caldav/principal/{{ calendar.principal }}/{{ calendar.id }}"></delete-button>
@@ -51,13 +51,13 @@
<h3>Deleted Calendars</h3> <h3>Deleted Calendars</h3>
<ul class="collection-list"> <ul class="collection-list">
{% for (meta, calendar) in deleted_calendars %} {% for (meta, calendar) in deleted_calendars %}
{% let color = calendar.color.to_owned().unwrap_or("transparent".to_owned()) %} {% let color = calendar.meta.color.to_owned().unwrap_or("transparent".to_owned()) %}
<li class="collection-list-item" style="--color: {{ color }}"> <li class="collection-list-item" style="--color: {{ color }}">
<a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"></a> <a href="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}"></a>
<div class="inner"> <div class="inner">
<span class="title"> <span class="title">
{%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%} {%- if calendar.principal != user.id -%}{{ calendar.principal }}/{%- endif -%}
{{ calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }} {{ calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) }}
<div class="comps"> <div class="comps">
{% for comp in calendar.components %} {% for comp in calendar.components %}
<span>{{ comp }}</span> <span>{{ comp }}</span>
@@ -65,7 +65,7 @@
</div> </div>
</span> </span>
<span class="description"> <span class="description">
{% if let Some(description) = calendar.description %}{{ description }}{% endif %} {% if let Some(description) = calendar.meta.description %}{{ description }}{% endif %}
</span> </span>
<div class="actions"> <div class="actions">
<form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST" <form action="/frontend/user/{{ calendar.principal }}/calendar/{{ calendar.id}}/restore" method="POST"

View File

@@ -4,9 +4,9 @@
{% endblock %} {% endblock %}
{% block content %} {% block content %}
{% let name = calendar.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %} {% let name = calendar.meta.displayname.to_owned().unwrap_or(calendar.id.to_owned()) %}
<h1>{{ calendar.principal }}/{{ name }}</h1> <h1>{{ calendar.principal }}/{{ name }}</h1>
{% if let Some(description) = calendar.description %}<p>{{ description }}</p>{% endif%} {% if let Some(description) = calendar.meta.description %}<p>{{ description }}</p>{% endif%}
{% if let Some(subscription_url) = calendar.subscription_url %} {% if let Some(subscription_url) = calendar.subscription_url %}
<h2>Subscription URL</h2> <h2>Subscription URL</h2>

View File

@@ -6,13 +6,23 @@ use rustical_ical::CalendarObjectType;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Serialize, Deserialize)] #[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Calendar { pub struct CalendarMetadata {
pub principal: String, // Attributes that may be outsourced
pub id: String,
pub displayname: Option<String>, pub displayname: Option<String>,
pub order: i64, pub order: i64,
pub description: Option<String>, pub description: Option<String>,
pub color: Option<String>, pub color: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Calendar {
// Attributes that may be outsourced
#[serde(flatten)]
pub meta: CalendarMetadata,
// Common calendar attributes
pub principal: String,
pub id: String,
pub timezone_id: Option<String>, pub timezone_id: Option<String>,
pub deleted_at: Option<NaiveDateTime>, pub deleted_at: Option<NaiveDateTime>,
pub synctoken: i64, pub synctoken: i64,

View File

@@ -1,4 +1,6 @@
use crate::{Addressbook, AddressbookStore, Calendar, CalendarStore, Error}; use crate::{
Addressbook, AddressbookStore, Calendar, CalendarStore, Error, calendar::CalendarMetadata,
};
use async_trait::async_trait; use async_trait::async_trait;
use derive_more::derive::Constructor; use derive_more::derive::Constructor;
use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType}; use rustical_ical::{AddressObject, CalendarObject, CalendarObjectType};
@@ -14,12 +16,14 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar {
Calendar { Calendar {
principal: addressbook.principal, principal: addressbook.principal,
id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id), id: format!("{}{}", BIRTHDAYS_PREFIX, addressbook.id),
displayname: addressbook meta: CalendarMetadata {
.displayname displayname: addressbook
.map(|name| format!("{name} birthdays")), .displayname
order: 0, .map(|name| format!("{name} birthdays")),
description: None, order: 0,
color: None, description: None,
color: None,
},
timezone_id: None, timezone_id: None,
deleted_at: addressbook.deleted_at, deleted_at: addressbook.deleted_at,
synctoken: addressbook.synctoken, synctoken: addressbook.synctoken,

View File

@@ -22,7 +22,7 @@ pub use secret::Secret;
pub use subscription_store::*; pub use subscription_store::*;
pub use addressbook::Addressbook; pub use addressbook::Addressbook;
pub use calendar::Calendar; pub use calendar::{Calendar, CalendarMetadata};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum CollectionOperationInfo { pub enum CollectionOperationInfo {

View File

@@ -5,7 +5,7 @@ use derive_more::derive::Constructor;
use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType}; use rustical_ical::{CalDateTime, CalendarObject, CalendarObjectType};
use rustical_store::calendar_store::CalendarQuery; use rustical_store::calendar_store::CalendarQuery;
use rustical_store::synctoken::format_synctoken; use rustical_store::synctoken::format_synctoken;
use rustical_store::{Calendar, CalendarStore, CollectionMetadata, Error}; use rustical_store::{Calendar, CalendarMetadata, CalendarStore, CollectionMetadata, Error};
use rustical_store::{CollectionOperation, CollectionOperationInfo}; use rustical_store::{CollectionOperation, CollectionOperationInfo};
use sqlx::types::chrono::NaiveDateTime; use sqlx::types::chrono::NaiveDateTime;
use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction}; use sqlx::{Acquire, Executor, Sqlite, SqlitePool, Transaction};
@@ -69,10 +69,12 @@ impl From<CalendarRow> for Calendar {
Self { Self {
principal: value.principal, principal: value.principal,
id: value.id, id: value.id,
displayname: value.displayname, meta: CalendarMetadata {
order: value.order, displayname: value.displayname,
description: value.description, order: value.order,
color: value.color, description: value.description,
color: value.color,
},
timezone_id: value.timezone_id, timezone_id: value.timezone_id,
deleted_at: value.deleted_at, deleted_at: value.deleted_at,
synctoken: value.synctoken, synctoken: value.synctoken,
@@ -159,10 +161,10 @@ impl SqliteCalendarStore {
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#, VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
calendar.principal, calendar.principal,
calendar.id, calendar.id,
calendar.displayname, calendar.meta.displayname,
calendar.description, calendar.meta.description,
calendar.order, calendar.meta.order,
calendar.color, calendar.meta.color,
calendar.subscription_url, calendar.subscription_url,
calendar.timezone_id, calendar.timezone_id,
calendar.push_topic, calendar.push_topic,
@@ -189,10 +191,10 @@ impl SqliteCalendarStore {
WHERE (principal, id) = (?, ?)"#, WHERE (principal, id) = (?, ?)"#,
calendar.principal, calendar.principal,
calendar.id, calendar.id,
calendar.displayname, calendar.meta.displayname,
calendar.description, calendar.meta.description,
calendar.order, calendar.meta.order,
calendar.color, calendar.meta.color,
calendar.timezone_id, calendar.timezone_id,
calendar.push_topic, calendar.push_topic,
comp_event, comp_todo, comp_journal, comp_event, comp_todo, comp_journal,