mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 21:42:34 +00:00
Calendar data model: Switch to only saving timezone id
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3282,6 +3282,7 @@ dependencies = [
|
|||||||
"tower",
|
"tower",
|
||||||
"tower-sessions",
|
"tower-sessions",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"vtimezones-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ use crate::calendar::prop::SupportedCalendarComponentSet;
|
|||||||
use axum::extract::{Path, State};
|
use axum::extract::{Path, State};
|
||||||
use axum::response::{IntoResponse, Response};
|
use axum::response::{IntoResponse, Response};
|
||||||
use http::{Method, StatusCode};
|
use http::{Method, StatusCode};
|
||||||
|
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;
|
||||||
@@ -82,26 +83,38 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
|
|||||||
request.displayname = None
|
request.displayname = None
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut timezone = request.calendar_timezone;
|
let timezone_id = if let Some(tzid) = request.calendar_timezone_id {
|
||||||
if let Some(tzid) = request.calendar_timezone_id.as_ref() {
|
Some(tzid)
|
||||||
// Validate timezone id and set timezone accordingly
|
} else if let Some(tz) = request.calendar_timezone {
|
||||||
timezone = Some(
|
// TODO: Proper error (calendar-timezone precondition)
|
||||||
vtimezones_rs::VTIMEZONES
|
let calendar = IcalParser::new(tz.as_bytes())
|
||||||
.get(tzid)
|
.next()
|
||||||
.ok_or(rustical_dav::Error::BadRequest(format!(
|
.ok_or(rustical_dav::Error::BadRequest(
|
||||||
"Invalid timezone-id: {tzid}"
|
"No timezone data provided".to_owned(),
|
||||||
)))?
|
))?
|
||||||
.to_string(),
|
.map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?;
|
||||||
);
|
|
||||||
}
|
let timezone = calendar
|
||||||
|
.timezones
|
||||||
|
.first()
|
||||||
|
.ok_or(rustical_dav::Error::BadRequest(
|
||||||
|
"No timezone data provided".to_owned(),
|
||||||
|
))?;
|
||||||
|
let timezone: chrono_tz::Tz = timezone
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?;
|
||||||
|
|
||||||
|
Some(timezone.name().to_owned())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
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),
|
order: request.calendar_order.unwrap_or(0),
|
||||||
displayname: request.displayname,
|
displayname: request.displayname,
|
||||||
timezone,
|
timezone_id,
|
||||||
timezone_id: request.calendar_timezone_id,
|
|
||||||
color: request.calendar_color,
|
color: request.calendar_color,
|
||||||
description: request.calendar_description,
|
description: request.calendar_description,
|
||||||
deleted_at: None,
|
deleted_at: None,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ use crate::Error;
|
|||||||
use crate::calendar::prop::ReportMethod;
|
use crate::calendar::prop::ReportMethod;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use derive_more::derive::{From, Into};
|
use derive_more::derive::{From, Into};
|
||||||
|
use ical::IcalParser;
|
||||||
use rustical_dav::extensions::{
|
use rustical_dav::extensions::{
|
||||||
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
|
CommonPropertiesExtension, CommonPropertiesProp, SyncTokenExtension, SyncTokenExtensionProp,
|
||||||
};
|
};
|
||||||
@@ -132,7 +133,9 @@ impl Resource for CalendarResource {
|
|||||||
CalendarProp::CalendarDescription(self.cal.description.clone())
|
CalendarProp::CalendarDescription(self.cal.description.clone())
|
||||||
}
|
}
|
||||||
CalendarPropName::CalendarTimezone => {
|
CalendarPropName::CalendarTimezone => {
|
||||||
CalendarProp::CalendarTimezone(self.cal.timezone.clone())
|
CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| {
|
||||||
|
vtimezones_rs::VTIMEZONES.get(tzid).map(|tz| tz.to_string())
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
// chrono_tz uses the IANA database
|
// chrono_tz uses the IANA database
|
||||||
CalendarPropName::TimezoneServiceSet => CalendarProp::TimezoneServiceSet(
|
CalendarPropName::TimezoneServiceSet => CalendarProp::TimezoneServiceSet(
|
||||||
@@ -191,23 +194,42 @@ impl Resource for CalendarResource {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarProp::CalendarTimezone(timezone) => {
|
CalendarProp::CalendarTimezone(timezone) => {
|
||||||
// TODO: Ensure that timezone-id is also updated
|
if let Some(tz) = timezone {
|
||||||
// We probably want to prohibit non-IANA timezones
|
// TODO: Proper error (calendar-timezone precondition)
|
||||||
self.cal.timezone = timezone;
|
let calendar = IcalParser::new(tz.as_bytes())
|
||||||
|
.next()
|
||||||
|
.ok_or(rustical_dav::Error::BadRequest(
|
||||||
|
"No timezone data provided".to_owned(),
|
||||||
|
))?
|
||||||
|
.map_err(|_| {
|
||||||
|
rustical_dav::Error::BadRequest(
|
||||||
|
"No timezone data provided".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let timezone =
|
||||||
|
calendar
|
||||||
|
.timezones
|
||||||
|
.first()
|
||||||
|
.ok_or(rustical_dav::Error::BadRequest(
|
||||||
|
"No timezone data provided".to_owned(),
|
||||||
|
))?;
|
||||||
|
let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| {
|
||||||
|
rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
|
||||||
|
})?;
|
||||||
|
|
||||||
|
self.cal.timezone_id = Some(timezone.name().to_owned());
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly),
|
CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly),
|
||||||
CalendarProp::CalendarTimezoneId(timezone_id) => {
|
CalendarProp::CalendarTimezoneId(timezone_id) => {
|
||||||
if let Some(tzid) = &timezone_id {
|
if let Some(tzid) = &timezone_id {
|
||||||
// Validate timezone id and set timezone accordingly
|
if !vtimezones_rs::VTIMEZONES.contains_key(tzid) {
|
||||||
self.cal.timezone = Some(
|
return Err(rustical_dav::Error::BadRequest(format!(
|
||||||
vtimezones_rs::VTIMEZONES
|
"Invalid timezone-id: {tzid}"
|
||||||
.get(tzid)
|
)));
|
||||||
.ok_or(rustical_dav::Error::BadRequest(format!(
|
}
|
||||||
"Invalid timezone-id: {tzid}"
|
|
||||||
)))?
|
|
||||||
.to_string(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
self.cal.timezone_id = timezone_id;
|
self.cal.timezone_id = timezone_id;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -248,15 +270,11 @@ impl Resource for CalendarResource {
|
|||||||
self.cal.description = None;
|
self.cal.description = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
CalendarPropName::CalendarTimezone => {
|
CalendarPropName::CalendarTimezone | CalendarPropName::CalendarTimezoneId => {
|
||||||
self.cal.timezone = None;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
|
|
||||||
CalendarPropName::CalendarTimezoneId => {
|
|
||||||
self.cal.timezone_id = None;
|
self.cal.timezone_id = None;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
|
||||||
CalendarPropName::CalendarOrder => {
|
CalendarPropName::CalendarOrder => {
|
||||||
self.cal.order = 0;
|
self.cal.order = 0;
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
{% if let Some(timezone_id) = calendar.timezone_id %}
|
{% if let Some(timezone_id) = calendar.timezone_id %}
|
||||||
<p>{{ timezone_id }}</p>
|
<p>{{ timezone_id }}</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if let Some(timezone) = calendar.timezone %}
|
{% if let Some(timezone) = calendar.get_vtimezone() %}
|
||||||
<textarea rows="16" readonly>{{ timezone }}</textarea>
|
<textarea rows="16" readonly>{{ timezone }}</textarea>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ headers.workspace = true
|
|||||||
tower.workspace = true
|
tower.workspace = true
|
||||||
futures-core.workspace = true
|
futures-core.workspace = true
|
||||||
tower-sessions.workspace = true
|
tower-sessions.workspace = true
|
||||||
|
vtimezones-rs.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
rstest = { workspace = true }
|
rstest = { workspace = true }
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
use crate::synctoken::format_synctoken;
|
use crate::synctoken::format_synctoken;
|
||||||
use chrono::NaiveDateTime;
|
use chrono::NaiveDateTime;
|
||||||
use rustical_ical::CalendarObjectType;
|
use rustical_ical::CalendarObjectType;
|
||||||
@@ -11,7 +13,6 @@ pub struct Calendar {
|
|||||||
pub order: i64,
|
pub order: i64,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub color: Option<String>,
|
pub color: Option<String>,
|
||||||
pub timezone: Option<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,
|
||||||
@@ -24,4 +25,16 @@ impl Calendar {
|
|||||||
pub fn format_synctoken(&self) -> String {
|
pub fn format_synctoken(&self) -> String {
|
||||||
format_synctoken(self.synctoken)
|
format_synctoken(self.synctoken)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_timezone(&self) -> Option<chrono_tz::Tz> {
|
||||||
|
self.timezone_id
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|tzid| chrono_tz::Tz::from_str(tzid).ok())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_vtimezone(&self) -> Option<&'static str> {
|
||||||
|
self.timezone_id
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|tzid| vtimezones_rs::VTIMEZONES.get(tzid).cloned())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ fn birthday_calendar(addressbook: Addressbook) -> Calendar {
|
|||||||
order: 0,
|
order: 0,
|
||||||
description: None,
|
description: None,
|
||||||
color: None,
|
color: None,
|
||||||
timezone: None,
|
|
||||||
timezone_id: None,
|
timezone_id: None,
|
||||||
deleted_at: addressbook.deleted_at,
|
deleted_at: addressbook.deleted_at,
|
||||||
synctoken: addressbook.synctoken,
|
synctoken: addressbook.synctoken,
|
||||||
|
|||||||
@@ -44,7 +44,6 @@ struct CalendarRow {
|
|||||||
order: i64,
|
order: i64,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
color: Option<String>,
|
color: Option<String>,
|
||||||
timezone: Option<String>,
|
|
||||||
timezone_id: Option<String>,
|
timezone_id: Option<String>,
|
||||||
deleted_at: Option<NaiveDateTime>,
|
deleted_at: Option<NaiveDateTime>,
|
||||||
synctoken: i64,
|
synctoken: i64,
|
||||||
@@ -74,7 +73,6 @@ impl From<CalendarRow> for Calendar {
|
|||||||
order: value.order,
|
order: value.order,
|
||||||
description: value.description,
|
description: value.description,
|
||||||
color: value.color,
|
color: value.color,
|
||||||
timezone: value.timezone,
|
|
||||||
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,
|
||||||
@@ -100,7 +98,7 @@ impl SqliteCalendarStore {
|
|||||||
) -> Result<Calendar, Error> {
|
) -> Result<Calendar, Error> {
|
||||||
let cal = sqlx::query_as!(
|
let cal = sqlx::query_as!(
|
||||||
CalendarRow,
|
CalendarRow,
|
||||||
r#"SELECT *
|
r#"SELECT principal, id, displayname, "order", description, color, timezone_id, deleted_at, synctoken, subscription_url, push_topic, comp_event, comp_todo, comp_journal
|
||||||
FROM calendars
|
FROM calendars
|
||||||
WHERE (principal, id) = (?, ?)
|
WHERE (principal, id) = (?, ?)
|
||||||
AND ((deleted_at IS NULL) OR ?) "#,
|
AND ((deleted_at IS NULL) OR ?) "#,
|
||||||
@@ -120,7 +118,7 @@ impl SqliteCalendarStore {
|
|||||||
) -> Result<Vec<Calendar>, Error> {
|
) -> Result<Vec<Calendar>, Error> {
|
||||||
let cals = sqlx::query_as!(
|
let cals = sqlx::query_as!(
|
||||||
CalendarRow,
|
CalendarRow,
|
||||||
r#"SELECT *
|
r#"SELECT principal, id, displayname, "order", description, color, timezone_id, deleted_at, synctoken, subscription_url, push_topic, comp_event, comp_todo, comp_journal
|
||||||
FROM calendars
|
FROM calendars
|
||||||
WHERE principal = ? AND deleted_at IS NULL"#,
|
WHERE principal = ? AND deleted_at IS NULL"#,
|
||||||
principal
|
principal
|
||||||
@@ -137,7 +135,7 @@ impl SqliteCalendarStore {
|
|||||||
) -> Result<Vec<Calendar>, Error> {
|
) -> Result<Vec<Calendar>, Error> {
|
||||||
let cals = sqlx::query_as!(
|
let cals = sqlx::query_as!(
|
||||||
CalendarRow,
|
CalendarRow,
|
||||||
r#"SELECT *
|
r#"SELECT principal, id, displayname, "order", description, color, timezone_id, deleted_at, synctoken, subscription_url, push_topic, comp_event, comp_todo, comp_journal
|
||||||
FROM calendars
|
FROM calendars
|
||||||
WHERE principal = ? AND deleted_at IS NOT NULL"#,
|
WHERE principal = ? AND deleted_at IS NOT NULL"#,
|
||||||
principal
|
principal
|
||||||
@@ -157,8 +155,8 @@ impl SqliteCalendarStore {
|
|||||||
let comp_journal = calendar.components.contains(&CalendarObjectType::Journal);
|
let comp_journal = calendar.components.contains(&CalendarObjectType::Journal);
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
r#"INSERT INTO calendars (principal, id, displayname, description, "order", color, subscription_url, timezone, timezone_id, push_topic, comp_event, comp_todo, comp_journal)
|
r#"INSERT INTO calendars (principal, id, displayname, description, "order", color, subscription_url, timezone_id, push_topic, comp_event, comp_todo, comp_journal)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"#,
|
||||||
calendar.principal,
|
calendar.principal,
|
||||||
calendar.id,
|
calendar.id,
|
||||||
calendar.displayname,
|
calendar.displayname,
|
||||||
@@ -166,7 +164,6 @@ impl SqliteCalendarStore {
|
|||||||
calendar.order,
|
calendar.order,
|
||||||
calendar.color,
|
calendar.color,
|
||||||
calendar.subscription_url,
|
calendar.subscription_url,
|
||||||
calendar.timezone,
|
|
||||||
calendar.timezone_id,
|
calendar.timezone_id,
|
||||||
calendar.push_topic,
|
calendar.push_topic,
|
||||||
comp_event, comp_todo, comp_journal
|
comp_event, comp_todo, comp_journal
|
||||||
@@ -188,7 +185,7 @@ impl SqliteCalendarStore {
|
|||||||
let comp_journal = calendar.components.contains(&CalendarObjectType::Journal);
|
let comp_journal = calendar.components.contains(&CalendarObjectType::Journal);
|
||||||
|
|
||||||
let result = sqlx::query!(
|
let result = sqlx::query!(
|
||||||
r#"UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, "order" = ?, color = ?, timezone = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ?
|
r#"UPDATE calendars SET principal = ?, id = ?, displayname = ?, description = ?, "order" = ?, color = ?, timezone_id = ?, push_topic = ?, comp_event = ?, comp_todo = ?, comp_journal = ?
|
||||||
WHERE (principal, id) = (?, ?)"#,
|
WHERE (principal, id) = (?, ?)"#,
|
||||||
calendar.principal,
|
calendar.principal,
|
||||||
calendar.id,
|
calendar.id,
|
||||||
@@ -196,7 +193,6 @@ impl SqliteCalendarStore {
|
|||||||
calendar.description,
|
calendar.description,
|
||||||
calendar.order,
|
calendar.order,
|
||||||
calendar.color,
|
calendar.color,
|
||||||
calendar.timezone,
|
|
||||||
calendar.timezone_id,
|
calendar.timezone_id,
|
||||||
calendar.push_topic,
|
calendar.push_topic,
|
||||||
comp_event, comp_todo, comp_journal,
|
comp_event, comp_todo, comp_journal,
|
||||||
|
|||||||
Reference in New Issue
Block a user