Compare commits
No commits in common. "b361c1ab5815bf612fa7fdd3e301145ef26959ca" and "79a054ab40387ea3d093845e67aba738bbd5e4a7" have entirely different histories.
b361c1ab58
...
79a054ab40
13 changed files with 172 additions and 307 deletions
57
Cargo.lock
generated
57
Cargo.lock
generated
|
|
@ -1280,47 +1280,6 @@ version = "1.0.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jiff"
|
|
||||||
version = "0.2.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
|
|
||||||
dependencies = [
|
|
||||||
"jiff-static",
|
|
||||||
"jiff-tzdb-platform",
|
|
||||||
"log",
|
|
||||||
"portable-atomic",
|
|
||||||
"portable-atomic-util",
|
|
||||||
"serde_core",
|
|
||||||
"windows-sys 0.61.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jiff-static"
|
|
||||||
version = "0.2.23"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.106",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jiff-tzdb"
|
|
||||||
version = "0.1.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "jiff-tzdb-platform"
|
|
||||||
version = "0.1.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
|
|
||||||
dependencies = [
|
|
||||||
"jiff-tzdb",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "js-sys"
|
name = "js-sys"
|
||||||
version = "0.3.81"
|
version = "0.3.81"
|
||||||
|
|
@ -1468,7 +1427,6 @@ dependencies = [
|
||||||
"http",
|
"http",
|
||||||
"icalendar",
|
"icalendar",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"jiff",
|
|
||||||
"listenfd",
|
"listenfd",
|
||||||
"markdown",
|
"markdown",
|
||||||
"maud",
|
"maud",
|
||||||
|
|
@ -1889,21 +1847,6 @@ version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "portable-atomic"
|
|
||||||
version = "1.13.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "portable-atomic-util"
|
|
||||||
version = "0.2.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
|
|
||||||
dependencies = [
|
|
||||||
"portable-atomic",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "potential_utf"
|
name = "potential_utf"
|
||||||
version = "0.1.3"
|
version = "0.1.3"
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ clap = { version = "4.5.53", features = ["derive"] }
|
||||||
http = "1.3.1"
|
http = "1.3.1"
|
||||||
icalendar = "0.17.5"
|
icalendar = "0.17.5"
|
||||||
itertools = "0.14.0"
|
itertools = "0.14.0"
|
||||||
jiff = { version = "0.2.23", features = ["serde"] }
|
|
||||||
listenfd = "1.0.2"
|
listenfd = "1.0.2"
|
||||||
markdown = "1.0.0"
|
markdown = "1.0.0"
|
||||||
maud = { version = "0.27.0", features = ["axum"] }
|
maud = { version = "0.27.0", features = ["axum"] }
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ test.skip("groups wrap nicely", async ({ page }) => {
|
||||||
// that the text is all on one line. Manual inspection looks good at time of writing.
|
// that the text is all on one line. Manual inspection looks good at time of writing.
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allow marking as inactive', async ({ page }) => {
|
test('allow marking as hidden', async ({ page }) => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -45,20 +45,15 @@ test('allow exempting from stale', async ({ page }) => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('stale list considers periodicity', async ({ page }) => {
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
test('page title has contact primary name', async ({ page }) => {
|
|
||||||
await expect(page.title()).toContain("Test Testerson");
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
|
||||||
test('bullet points in free text display well', async ({ page }) => {
|
test('bullet points in free text display well', async ({ page }) => {
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
twst('page title has contact primary name', async ({ page }) => {
|
||||||
|
await expect(page.title()).toContain("Test Testerson");
|
||||||
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
home: contact list scrolls in screen, not off screen
|
home: contact list scrolls in screen, not off screen
|
||||||
home: clicking off contact list closes it
|
home: clicking off contact list closes it
|
||||||
home: contact list is sorted ignoring case
|
home: contact list is sorted ignoring case
|
||||||
|
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
alter table contacts add column
|
|
||||||
can_stale boolean not null default true;
|
|
||||||
|
|
||||||
alter table contacts add column
|
|
||||||
periodicity text not null default 'P0D';
|
|
||||||
|
|
||||||
alter table contacts add column
|
|
||||||
active boolean not null default true;
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use jiff::{Timestamp, Unit, Zoned, civil, tz::TimeZone};
|
use chrono::Local;
|
||||||
use sqlx::sqlite::SqliteRow;
|
use sqlx::sqlite::SqliteRow;
|
||||||
use sqlx::{FromRow, Row};
|
use sqlx::{FromRow, Row};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
@ -29,31 +29,25 @@ impl Display for Birthday {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Birthday {
|
impl Birthday {
|
||||||
pub fn next_occurrence(&self) -> Option<civil::Date> {
|
pub fn next_occurrence(&self) -> Option<chrono::NaiveDate> {
|
||||||
match &self {
|
match &self {
|
||||||
Birthday::Text(_) => None,
|
Birthday::Text(_) => None,
|
||||||
Birthday::Date(date) => date.next_month_day_occurrence(),
|
Birthday::Date(date) => Some(date.next_month_day_occurrence()?),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn until_next(&self) -> Option<jiff::Span> {
|
pub fn until_next(&self) -> Option<chrono::TimeDelta> {
|
||||||
self.next_occurrence()
|
self.next_occurrence()
|
||||||
.map(|when| when.since(Zoned::now().date()).ok())?
|
.map(|when| when.signed_duration_since(Local::now().date_naive()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// None if this is a text birthday or doesn't have a year
|
/// None if this is a text birthday or doesn't have a year
|
||||||
pub fn age(&self) -> Option<i32> {
|
pub fn age(&self) -> Option<u32> {
|
||||||
match &self {
|
match &self {
|
||||||
Birthday::Text(_) => None,
|
Birthday::Text(_) => None,
|
||||||
Birthday::Date(date) => {
|
Birthday::Date(date) => date
|
||||||
let now = Timestamp::now().to_zoned(TimeZone::UTC);
|
.to_date_naive()
|
||||||
date.to_civil_date().map(|birthdate| {
|
.map(|birthdate| Local::now().date_naive().years_since(birthdate))?,
|
||||||
now.since(&birthdate.to_zoned(TimeZone::UTC).unwrap())
|
|
||||||
.unwrap()
|
|
||||||
.total((Unit::Year, &now))
|
|
||||||
.unwrap() as i32
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use jiff::{Span, Timestamp, civil::Date};
|
use chrono::{DateTime, NaiveDate, Utc};
|
||||||
use sqlx::sqlite::SqlitePool;
|
use sqlx::sqlite::SqlitePool;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
|
|
@ -12,20 +12,14 @@ struct RawContact {
|
||||||
birthday: Option<String>,
|
birthday: Option<String>,
|
||||||
manually_freshened_at: Option<String>,
|
manually_freshened_at: Option<String>,
|
||||||
lives_with: String,
|
lives_with: String,
|
||||||
can_stale: bool,
|
|
||||||
active: bool,
|
|
||||||
periodicity: String,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Contact {
|
pub struct Contact {
|
||||||
pub id: DbId,
|
pub id: DbId,
|
||||||
pub birthday: Option<Birthday>,
|
pub birthday: Option<Birthday>,
|
||||||
pub manually_freshened_at: Option<Timestamp>,
|
pub manually_freshened_at: Option<DateTime<Utc>>,
|
||||||
pub lives_with: String,
|
pub lives_with: String,
|
||||||
pub can_stale: bool,
|
|
||||||
pub active: bool,
|
|
||||||
pub periodicity: Span,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Into<Contact> for RawContact {
|
impl Into<Contact> for RawContact {
|
||||||
|
|
@ -37,11 +31,9 @@ impl Into<Contact> for RawContact {
|
||||||
.and_then(|s| Birthday::from_str(s.as_ref()).ok()),
|
.and_then(|s| Birthday::from_str(s.as_ref()).ok()),
|
||||||
manually_freshened_at: self
|
manually_freshened_at: self
|
||||||
.manually_freshened_at
|
.manually_freshened_at
|
||||||
.and_then(|str| str.parse::<Timestamp>().ok()),
|
.and_then(|str| DateTime::parse_from_str(str.as_ref(), "%+").ok())
|
||||||
|
.map(|d| d.to_utc()),
|
||||||
lives_with: self.lives_with,
|
lives_with: self.lives_with,
|
||||||
can_stale: self.can_stale,
|
|
||||||
active: self.active,
|
|
||||||
periodicity: self.periodicity.parse().unwrap(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -51,10 +43,6 @@ struct RawHydratedContact {
|
||||||
birthday: Option<String>,
|
birthday: Option<String>,
|
||||||
manually_freshened_at: Option<String>,
|
manually_freshened_at: Option<String>,
|
||||||
lives_with: String,
|
lives_with: String,
|
||||||
can_stale: bool,
|
|
||||||
active: bool,
|
|
||||||
periodicity: String,
|
|
||||||
|
|
||||||
last_mention_date: Option<String>,
|
last_mention_date: Option<String>,
|
||||||
names: Option<String>,
|
names: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
@ -62,7 +50,7 @@ struct RawHydratedContact {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct HydratedContact {
|
pub struct HydratedContact {
|
||||||
pub contact: Contact,
|
pub contact: Contact,
|
||||||
pub last_mention_date: Option<Date>,
|
pub last_mention_date: Option<NaiveDate>,
|
||||||
pub names: Vec<String>,
|
pub names: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,9 +62,6 @@ impl Into<HydratedContact> for RawHydratedContact {
|
||||||
birthday: self.birthday,
|
birthday: self.birthday,
|
||||||
manually_freshened_at: self.manually_freshened_at,
|
manually_freshened_at: self.manually_freshened_at,
|
||||||
lives_with: self.lives_with,
|
lives_with: self.lives_with,
|
||||||
can_stale: self.can_stale,
|
|
||||||
active: self.active,
|
|
||||||
periodicity: self.periodicity,
|
|
||||||
}),
|
}),
|
||||||
names: self
|
names: self
|
||||||
.names
|
.names
|
||||||
|
|
@ -86,7 +71,7 @@ impl Into<HydratedContact> for RawHydratedContact {
|
||||||
.collect::<Vec<String>>(),
|
.collect::<Vec<String>>(),
|
||||||
last_mention_date: self
|
last_mention_date: self
|
||||||
.last_mention_date
|
.last_mention_date
|
||||||
.and_then(|str| str.parse::<Date>().ok()),
|
.and_then(|str| NaiveDate::from_str(str.as_ref()).ok()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -107,27 +92,11 @@ impl HydratedContact {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn status(&self) -> &'static str {
|
|
||||||
if self.can_stale {
|
|
||||||
if self.active { "normal" } else { "inactive" }
|
|
||||||
} else {
|
|
||||||
"permanent"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn load(id: DbId, pool: &SqlitePool) -> Result<Self, AppError> {
|
pub async fn load(id: DbId, pool: &SqlitePool) -> Result<Self, AppError> {
|
||||||
// copy-paste the query from 'all', then add "where c.id = $2" to the last line
|
// copy-paste the query from 'all', then add "where c.id = $2" to the last line
|
||||||
let raw = sqlx::query_as!(
|
let raw = sqlx::query_as!(
|
||||||
RawHydratedContact,
|
RawHydratedContact,
|
||||||
r#"select
|
r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", (
|
||||||
id,
|
|
||||||
birthday,
|
|
||||||
lives_with,
|
|
||||||
manually_freshened_at as "manually_freshened_at: String",
|
|
||||||
can_stale,
|
|
||||||
active,
|
|
||||||
periodicity,
|
|
||||||
(
|
|
||||||
select string_agg(name,x'1c' order by sort)
|
select string_agg(name,x'1c' order by sort)
|
||||||
from names where contact_id = c.id
|
from names where contact_id = c.id
|
||||||
) as names, (
|
) as names, (
|
||||||
|
|
@ -154,15 +123,7 @@ impl HydratedContact {
|
||||||
pub async fn all(pool: &SqlitePool) -> Result<Vec<Self>, AppError> {
|
pub async fn all(pool: &SqlitePool) -> Result<Vec<Self>, AppError> {
|
||||||
let contacts = sqlx::query_as!(
|
let contacts = sqlx::query_as!(
|
||||||
RawHydratedContact,
|
RawHydratedContact,
|
||||||
r#"select
|
r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", (
|
||||||
id,
|
|
||||||
birthday,
|
|
||||||
lives_with,
|
|
||||||
manually_freshened_at as "manually_freshened_at: String",
|
|
||||||
can_stale,
|
|
||||||
active,
|
|
||||||
periodicity,
|
|
||||||
(
|
|
||||||
select string_agg(name,x'1c' order by sort)
|
select string_agg(name,x'1c' order by sort)
|
||||||
from names where contact_id = c.id
|
from names where contact_id = c.id
|
||||||
) as names, (
|
) as names, (
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use jiff::civil::Date;
|
use chrono::NaiveDate;
|
||||||
use maud::{Markup, html};
|
use maud::{Markup, html};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use sqlx::sqlite::{SqlitePool, SqliteRow};
|
use sqlx::sqlite::{SqlitePool, SqliteRow};
|
||||||
|
|
@ -12,7 +12,7 @@ use crate::switchboard::{MentionHost, MentionHostType};
|
||||||
pub struct JournalEntry {
|
pub struct JournalEntry {
|
||||||
pub id: DbId,
|
pub id: DbId,
|
||||||
pub value: String,
|
pub value: String,
|
||||||
pub date: Date,
|
pub date: NaiveDate,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Into<MentionHost<'a>> for &'a JournalEntry {
|
impl<'a> Into<MentionHost<'a>> for &'a JournalEntry {
|
||||||
|
|
@ -69,7 +69,7 @@ impl FromRow<'_, SqliteRow> for JournalEntry {
|
||||||
let id: DbId = row.try_get("id")?;
|
let id: DbId = row.try_get("id")?;
|
||||||
let value: String = row.try_get("value")?;
|
let value: String = row.try_get("value")?;
|
||||||
let date_str: &str = row.try_get("date")?;
|
let date_str: &str = row.try_get("date")?;
|
||||||
let date: Date = date_str.parse().unwrap();
|
let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d").unwrap();
|
||||||
Ok(Self { id, value, date })
|
Ok(Self { id, value, date })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use jiff::{Timestamp, civil::Date, tz::TimeZone};
|
use chrono::{Datelike, Local, NaiveDate};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use sqlx::{Database, Decode, Encode, Sqlite, encode::IsNull};
|
use sqlx::{Database, Decode, Encode, Sqlite, encode::IsNull};
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
@ -6,39 +6,38 @@ use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct YearOptionalDate {
|
pub struct YearOptionalDate {
|
||||||
pub year: Option<i16>,
|
pub year: Option<i32>,
|
||||||
pub month: i8,
|
pub month: u32,
|
||||||
pub day: i8,
|
pub day: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl YearOptionalDate {
|
impl YearOptionalDate {
|
||||||
pub fn prev_month_day_occurrence(&self) -> Option<Date> {
|
pub fn prev_month_day_occurrence(&self) -> Option<NaiveDate> {
|
||||||
let now = Timestamp::now().to_zoned(TimeZone::UTC);
|
let now = Local::now();
|
||||||
let year = now.year();
|
let year = now.year();
|
||||||
Date::new(year, self.month, self.day).ok().and_then(|date| {
|
let mut date = NaiveDate::from_ymd_opt(year, self.month, self.day);
|
||||||
if date >= now.date() {
|
if let Some(real_date) = date {
|
||||||
Date::new(year - 1, self.month, self.day).ok()
|
if real_date >= now.date_naive() {
|
||||||
} else {
|
date = NaiveDate::from_ymd_opt(year - 1, self.month, self.day);
|
||||||
Some(date)
|
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
date
|
||||||
|
}
|
||||||
|
pub fn next_month_day_occurrence(&self) -> Option<NaiveDate> {
|
||||||
|
let now = Local::now();
|
||||||
|
let year = now.year();
|
||||||
|
let mut date = NaiveDate::from_ymd_opt(year, self.month, self.day);
|
||||||
|
if let Some(real_date) = date {
|
||||||
|
if real_date < now.date_naive() {
|
||||||
|
date = NaiveDate::from_ymd_opt(year + 1, self.month, self.day);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
date
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_month_day_occurrence(&self) -> Option<Date> {
|
pub fn to_date_naive(&self) -> Option<NaiveDate> {
|
||||||
let now = Timestamp::now().to_zoned(TimeZone::UTC);
|
|
||||||
let year = now.year();
|
|
||||||
Date::new(year, self.month, self.day).ok().and_then(|date| {
|
|
||||||
if date < now.date() {
|
|
||||||
Date::new(year + 1, self.month, self.day).ok()
|
|
||||||
} else {
|
|
||||||
Some(date)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_civil_date(&self) -> Option<Date> {
|
|
||||||
if let Some(year) = self.year {
|
if let Some(year) = self.year {
|
||||||
Date::new(year, self.month, self.day).ok()
|
NaiveDate::from_ymd_opt(year, self.month, self.day)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -69,12 +68,12 @@ impl FromStr for YearOptionalDate {
|
||||||
let date_re = Regex::new(r"^([0-9]{4}|--)([0-9]{2})([0-9]{2})$").unwrap();
|
let date_re = Regex::new(r"^([0-9]{4}|--)([0-9]{2})([0-9]{2})$").unwrap();
|
||||||
if let Some(caps) = date_re.captures(str) {
|
if let Some(caps) = date_re.captures(str) {
|
||||||
let year_str = &caps[1];
|
let year_str = &caps[1];
|
||||||
let month = i8::from_str(&caps[2]).unwrap();
|
let month = u32::from_str(&caps[2]).unwrap();
|
||||||
let day = i8::from_str(&caps[3]).unwrap();
|
let day = u32::from_str(&caps[3]).unwrap();
|
||||||
let year = if year_str == "--" {
|
let year = if year_str == "--" {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(i16::from_str(year_str).unwrap())
|
Some(i32::from_str(year_str).unwrap())
|
||||||
};
|
};
|
||||||
|
|
||||||
return Ok(Self { year, month, day });
|
return Ok(Self { year, month, day });
|
||||||
|
|
|
||||||
|
|
@ -109,6 +109,14 @@ impl Switchboard {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn remove(self: &mut Self, text: &String) {
|
||||||
|
self.trie.remove(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_mentionable(self: &mut Self, text: String, uri: String) {
|
||||||
|
self.trie.insert(text, uri);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn extract_mentions<'a>(&self, host: impl Into<MentionHost<'a>>) -> HashSet<Mention> {
|
pub fn extract_mentions<'a>(&self, host: impl Into<MentionHost<'a>>) -> HashSet<Mention> {
|
||||||
let host: MentionHost = host.into();
|
let host: MentionHost = host.into();
|
||||||
let name_re = Regex::new(r"\[\[(.+?)\]\]").unwrap();
|
let name_re = Regex::new(r"\[\[(.+?)\]\]").unwrap();
|
||||||
|
|
|
||||||
|
|
@ -7,19 +7,19 @@ use axum::{
|
||||||
};
|
};
|
||||||
use axum_extra::extract::Form;
|
use axum_extra::extract::Form;
|
||||||
use cache_bust::asset;
|
use cache_bust::asset;
|
||||||
use jiff::{Timestamp, Unit, tz::TimeZone};
|
use chrono::DateTime;
|
||||||
use maud::{Markup, html};
|
use maud::{Markup, html};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use slug::slugify;
|
use slug::slugify;
|
||||||
use sqlx::QueryBuilder;
|
use sqlx::{QueryBuilder, Sqlite};
|
||||||
|
|
||||||
use super::Layout;
|
use super::Layout;
|
||||||
use super::home::journal_section;
|
use super::home::journal_section;
|
||||||
use crate::db::DbId;
|
use crate::db::DbId;
|
||||||
use crate::models::user::AuthSession;
|
use crate::models::user::AuthSession;
|
||||||
use crate::models::{HydratedContact, JournalEntry};
|
use crate::models::{HydratedContact, JournalEntry};
|
||||||
use crate::switchboard::{MentionHost, MentionHostType, Switchboard, insert_mentions};
|
use crate::switchboard::{MentionHost, MentionHostType, insert_mentions, Switchboard};
|
||||||
use crate::{AppError, AppState};
|
use crate::{AppError, AppState};
|
||||||
|
|
||||||
pub mod fields;
|
pub mod fields;
|
||||||
|
|
@ -40,22 +40,22 @@ pub fn router() -> Router<AppState> {
|
||||||
.route("/contact/{contact_id}/edit", get(self::get::contact_edit))
|
.route("/contact/{contact_id}/edit", get(self::get::contact_edit))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn human_delta(span: &jiff::Span) -> String {
|
fn human_delta(delta: &chrono::TimeDelta) -> String {
|
||||||
let todate = Timestamp::now().to_zoned(TimeZone::UTC).date();
|
if delta.num_days() == 0 {
|
||||||
let span = span
|
return "today".to_string();
|
||||||
.round(
|
|
||||||
jiff::SpanRound::new()
|
|
||||||
.largest(Unit::Year)
|
|
||||||
.smallest(Unit::Day)
|
|
||||||
.relative(todate),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if span.is_zero() {
|
|
||||||
"today".to_string()
|
|
||||||
} else {
|
|
||||||
format!("in {:#}", span)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut result = "in ".to_string();
|
||||||
|
let mut rem = delta.clone();
|
||||||
|
if rem.num_days().abs() >= 7 {
|
||||||
|
let weeks = rem.num_days() / 7;
|
||||||
|
rem -= chrono::TimeDelta::days(weeks * 7);
|
||||||
|
result.push_str(&format!("{}w ", weeks));
|
||||||
|
}
|
||||||
|
if rem.num_days().abs() > 0 {
|
||||||
|
result.push_str(&format!("{}d ", rem.num_days()));
|
||||||
|
}
|
||||||
|
result.trim().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
mod get {
|
mod get {
|
||||||
|
|
@ -88,9 +88,7 @@ mod get {
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
let freshened = std::cmp::max(
|
let freshened = std::cmp::max(
|
||||||
contact
|
contact.manually_freshened_at.map(|when| when.date_naive()),
|
||||||
.manually_freshened_at
|
|
||||||
.map(|when| when.to_zoned(jiff::tz::TimeZone::UTC).date()),
|
|
||||||
entries.get(0).map(|entry| entry.date),
|
entries.get(0).map(|entry| entry.date),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -132,14 +130,6 @@ mod get {
|
||||||
div { (name) }
|
div { (name) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@if contact.status() != "normal" {
|
|
||||||
label { "status" }
|
|
||||||
div { (contact.status()) }
|
|
||||||
}
|
|
||||||
@if contact.status() == "normal" && contact.periodicity.is_positive() {
|
|
||||||
label { "periodicity" }
|
|
||||||
div { (format!("{:#}", contact.periodicity)) }
|
|
||||||
}
|
|
||||||
@if let Some(bday) = &contact.birthday {
|
@if let Some(bday) = &contact.birthday {
|
||||||
label { "birthday" }
|
label { "birthday" }
|
||||||
div {
|
div {
|
||||||
|
|
@ -223,7 +213,7 @@ mod get {
|
||||||
let mfresh_str = contact
|
let mfresh_str = contact
|
||||||
.manually_freshened_at
|
.manually_freshened_at
|
||||||
.clone()
|
.clone()
|
||||||
.map_or("".to_string(), |m| m.to_string());
|
.map_or("".to_string(), |m| m.to_rfc3339());
|
||||||
|
|
||||||
let text_body: String =
|
let text_body: String =
|
||||||
sqlx::query!("select text_body from contacts where id = $1", contact_id)
|
sqlx::query!("select text_body from contacts where id = $1", contact_id)
|
||||||
|
|
@ -243,7 +233,7 @@ mod get {
|
||||||
div #error;
|
div #error;
|
||||||
}
|
}
|
||||||
|
|
||||||
#fields x-data=(json!({ "status": contact.status() })){
|
div #fields {
|
||||||
label { @if contact.names.len() > 1 { "names" } @else { "name" }}
|
label { @if contact.names.len() > 1 { "names" } @else { "name" }}
|
||||||
div #names x-data=(format!("{{ names: {:?}, new_name: '' }}", &contact.names)) {
|
div #names x-data=(format!("{{ names: {:?}, new_name: '' }}", &contact.names)) {
|
||||||
template x-for="(name, idx) in names" {
|
template x-for="(name, idx) in names" {
|
||||||
|
|
@ -259,19 +249,6 @@ mod get {
|
||||||
input type="button" value="Add" x-on:click="names.push(new_name); new_name = ''";
|
input type="button" value="Add" x-on:click="names.push(new_name); new_name = ''";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
label { "status" }
|
|
||||||
div {
|
|
||||||
select name="status" x-model=("status") {
|
|
||||||
option value="normal" { "Normal" }
|
|
||||||
option value="permanent" { "Cannot go stale" }
|
|
||||||
option value="inactive" { "Inactive" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
label x-show="status === 'normal'" { "minimum stale time" }
|
|
||||||
div x-show="status === 'normal'"{
|
|
||||||
input name="periodicity" value=(format!("{:#}", contact.periodicity));
|
|
||||||
span .hint { code { "[0-9]+ (yr|mo|wk|day|h|m|s)" } "(" a href="https://docs.rs/jiff/latest/jiff/struct.Span.html#parsing-and-printing" { "details" } ")" }
|
|
||||||
}
|
|
||||||
label { "birthday" }
|
label { "birthday" }
|
||||||
div {
|
div {
|
||||||
input name="birthday" value=(contact.birthday.clone().map_or("".to_string(), |b| b.serialize()));
|
input name="birthday" value=(contact.birthday.clone().map_or("".to_string(), |b| b.serialize()));
|
||||||
|
|
@ -347,8 +324,6 @@ mod put {
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct PutContact {
|
pub struct PutContact {
|
||||||
name: Option<Vec<String>>,
|
name: Option<Vec<String>>,
|
||||||
status: String,
|
|
||||||
periodicity: Option<String>,
|
|
||||||
birthday: String,
|
birthday: String,
|
||||||
manually_freshened_at: String,
|
manually_freshened_at: String,
|
||||||
lives_with: String,
|
lives_with: String,
|
||||||
|
|
@ -376,22 +351,17 @@ mod put {
|
||||||
Some(payload.birthday)
|
Some(payload.birthday)
|
||||||
};
|
};
|
||||||
|
|
||||||
let manually_freshened_at: Option<String> = if payload.manually_freshened_at.is_empty() {
|
let manually_freshened_at = if payload.manually_freshened_at.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
Some(
|
Some(
|
||||||
payload
|
DateTime::parse_from_str(&payload.manually_freshened_at, "%+")
|
||||||
.manually_freshened_at
|
|
||||||
.parse::<Timestamp>()
|
|
||||||
.map_err(|_| anyhow::Error::msg("Could not parse freshened-at string"))?
|
.map_err(|_| anyhow::Error::msg("Could not parse freshened-at string"))?
|
||||||
.to_string(),
|
.to_utc()
|
||||||
|
.to_rfc3339(),
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
let active: bool = payload.status != "inactive";
|
|
||||||
let can_stale: bool = payload.status != "permanent";
|
|
||||||
let periodicity: String = payload.periodicity.unwrap_or("P0D".to_string());
|
|
||||||
|
|
||||||
let text_body = if payload.text_body.is_empty() {
|
let text_body = if payload.text_body.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -404,24 +374,21 @@ mod put {
|
||||||
|
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"update contacts set
|
"update contacts set
|
||||||
(
|
(birthday, manually_freshened_at, lives_with, text_body) =
|
||||||
birthday, manually_freshened_at, lives_with, text_body,
|
($1, $2, $3, $4)
|
||||||
active, can_stale, periodicity
|
where id = $5",
|
||||||
) =
|
|
||||||
(?, ?, ?, ?, ?, ?, ?)
|
|
||||||
where id = ?",
|
|
||||||
birthday,
|
birthday,
|
||||||
manually_freshened_at,
|
manually_freshened_at,
|
||||||
payload.lives_with,
|
payload.lives_with,
|
||||||
text_body,
|
text_body,
|
||||||
active,
|
|
||||||
can_stale,
|
|
||||||
periodicity,
|
|
||||||
contact_id
|
contact_id
|
||||||
)
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
if old_contact.text_body != text_body {
|
||||||
|
}
|
||||||
|
|
||||||
// these blocks are not in functions because payload gets progressively
|
// these blocks are not in functions because payload gets progressively
|
||||||
// partially moved as we handle each field and i don't want to deal with it
|
// partially moved as we handle each field and i don't want to deal with it
|
||||||
|
|
||||||
|
|
@ -521,18 +488,22 @@ mod put {
|
||||||
let old_names: Vec<String> = old_names.into_iter().map(|(s,)| s).collect();
|
let old_names: Vec<String> = old_names.into_iter().map(|(s,)| s).collect();
|
||||||
|
|
||||||
if old_names != new_names {
|
if old_names != new_names {
|
||||||
sqlx::query!("delete from names where contact_id = $1", contact_id)
|
sqlx::query!(
|
||||||
|
"delete from names where contact_id = $1",
|
||||||
|
contact_id
|
||||||
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if !new_names.is_empty() {
|
if !new_names.is_empty() {
|
||||||
QueryBuilder::new("insert into names (contact_id, sort, name) ")
|
QueryBuilder::new(
|
||||||
.push_values(new_names.iter().enumerate(), |mut b, (sort, name)| {
|
"insert into names (contact_id, sort, name) "
|
||||||
b.push_bind(contact_id)
|
).push_values(new_names.iter().enumerate(), |mut b, (sort, name)| {
|
||||||
|
b
|
||||||
|
.push_bind(contact_id)
|
||||||
.push_bind(DbId::try_from(sort).unwrap())
|
.push_bind(DbId::try_from(sort).unwrap())
|
||||||
.push_bind(name);
|
.push_bind(name);
|
||||||
})
|
}).build()
|
||||||
.build()
|
|
||||||
.persistent(false)
|
.persistent(false)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
@ -553,7 +524,10 @@ mod put {
|
||||||
let old_groups: Vec<String> = old_groups.into_iter().map(|(s,)| s).collect();
|
let old_groups: Vec<String> = old_groups.into_iter().map(|(s,)| s).collect();
|
||||||
|
|
||||||
if new_groups != old_groups {
|
if new_groups != old_groups {
|
||||||
sqlx::query!("delete from groups where contact_id = $1", contact_id)
|
sqlx::query!(
|
||||||
|
"delete from groups where contact_id = $1",
|
||||||
|
contact_id
|
||||||
|
)
|
||||||
.execute(pool)
|
.execute(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -592,6 +566,7 @@ mod put {
|
||||||
.await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if regen_text_body {
|
if regen_text_body {
|
||||||
sqlx::query!(
|
sqlx::query!(
|
||||||
"delete from mentions where entity_id = $1 and entity_type = $2",
|
"delete from mentions where entity_id = $1 and entity_type = $2",
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::response::IntoResponse;
|
use axum::response::IntoResponse;
|
||||||
use cache_bust::asset;
|
use cache_bust::asset;
|
||||||
use jiff::{Timestamp, Unit, Zoned, civil, tz::TimeZone};
|
use chrono::{Local, NaiveDate, TimeDelta};
|
||||||
use maud::{Markup, html};
|
use maud::{Markup, html};
|
||||||
use sqlx::sqlite::SqlitePool;
|
use sqlx::sqlite::SqlitePool;
|
||||||
|
|
||||||
|
|
@ -15,7 +15,7 @@ use crate::{AppError, AppState};
|
||||||
struct ContactFreshness {
|
struct ContactFreshness {
|
||||||
contact_id: DbId,
|
contact_id: DbId,
|
||||||
display: String,
|
display: String,
|
||||||
fresh_date: civil::Date,
|
fresh_date: NaiveDate,
|
||||||
fresh_str: String,
|
fresh_str: String,
|
||||||
elapsed_str: String,
|
elapsed_str: String,
|
||||||
}
|
}
|
||||||
|
|
@ -46,8 +46,8 @@ fn freshness_section(freshens: &Vec<ContactFreshness>) -> Result<Markup, AppErro
|
||||||
struct KnownBirthdayContact {
|
struct KnownBirthdayContact {
|
||||||
contact_id: i64,
|
contact_id: i64,
|
||||||
display: String,
|
display: String,
|
||||||
prev_birthday: civil::Date,
|
prev_birthday: NaiveDate,
|
||||||
next_birthday: civil::Date,
|
next_birthday: NaiveDate,
|
||||||
}
|
}
|
||||||
fn birthdays_section(
|
fn birthdays_section(
|
||||||
prev_birthdays: &Vec<KnownBirthdayContact>,
|
prev_birthdays: &Vec<KnownBirthdayContact>,
|
||||||
|
|
@ -64,7 +64,7 @@ fn birthdays_section(
|
||||||
(contact.display)
|
(contact.display)
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
(contact.next_birthday.strftime("%m-%d"))
|
(contact.next_birthday.format("%m-%d"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +75,7 @@ fn birthdays_section(
|
||||||
(contact.display)
|
(contact.display)
|
||||||
}
|
}
|
||||||
span {
|
span {
|
||||||
(contact.prev_birthday.strftime("%m-%d"))
|
(contact.prev_birthday.format("%m-%d"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -103,7 +103,7 @@ pub async fn journal_section(
|
||||||
added to the top of the list regardless of date; refresh the page to re-sort."
|
added to the top of the list regardless of date; refresh the page to re-sort."
|
||||||
}
|
}
|
||||||
form hx-post="/journal_entry" hx-target="next .entries" hx-target-error="#journal-error" hx-swap="afterbegin" hx-on::after-request="if(event.detail.successful) this.reset()" {
|
form hx-post="/journal_entry" hx-target="next .entries" hx-target-error="#journal-error" hx-swap="afterbegin" hx-on::after-request="if(event.detail.successful) this.reset()" {
|
||||||
input name="date" placeholder=(Zoned::now().date().to_string());
|
input name="date" placeholder=(Local::now().date_naive().to_string());
|
||||||
textarea name="value" placeholder="New entry..." autofocus {}
|
textarea name="value" placeholder="New entry..." autofocus {}
|
||||||
input type="submit" value="Add Entry";
|
input type="submit" value="Add Entry";
|
||||||
}
|
}
|
||||||
|
|
@ -134,60 +134,57 @@ pub mod get {
|
||||||
let mut freshens: Vec<ContactFreshness> = contacts
|
let mut freshens: Vec<ContactFreshness> = contacts
|
||||||
.clone()
|
.clone()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|contact| {
|
.map(|contact| {
|
||||||
if !contact.can_stale || !contact.active {
|
let zero = NaiveDate::from_epoch_days(0).unwrap();
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let zero = jiff::civil::Date::ZERO;
|
|
||||||
let fresh_date = std::cmp::max(
|
let fresh_date = std::cmp::max(
|
||||||
contact
|
contact
|
||||||
.manually_freshened_at
|
.manually_freshened_at
|
||||||
.map(|ts| ts.to_zoned(TimeZone::UTC).date())
|
.map(|x| x.date_naive())
|
||||||
.unwrap_or(zero),
|
.unwrap_or(zero),
|
||||||
contact.last_mention_date.unwrap_or(zero),
|
contact.last_mention_date.unwrap_or(zero),
|
||||||
);
|
);
|
||||||
if fresh_date == zero {
|
if fresh_date == zero {
|
||||||
Some(ContactFreshness {
|
ContactFreshness {
|
||||||
contact_id: contact.id,
|
contact_id: contact.id,
|
||||||
display: contact.display_name(),
|
display: contact.display_name(),
|
||||||
fresh_date,
|
fresh_date,
|
||||||
fresh_str: "never".to_string(),
|
fresh_str: "never".to_string(),
|
||||||
elapsed_str: "".to_string(),
|
elapsed_str: "".to_string(),
|
||||||
})
|
}
|
||||||
} else {
|
} else {
|
||||||
let utc = TimeZone::UTC;
|
let mut duration = Local::now().date_naive().signed_duration_since(fresh_date);
|
||||||
let todate = Timestamp::now().to_zoned(utc.clone()).date();
|
let mut elapsed: Vec<String> = Vec::new();
|
||||||
let elapsed = todate
|
let y = duration.num_weeks() / 52;
|
||||||
.since(&fresh_date.to_zoned(utc).unwrap())
|
let count = |n: i64, noun: &str| {
|
||||||
.unwrap()
|
format!("{} {}{}", n, noun, if n > 1 { "s" } else { "" })
|
||||||
.round(
|
};
|
||||||
jiff::SpanRound::new()
|
if y > 0 {
|
||||||
.largest(Unit::Year)
|
elapsed.push(count(y, "year"));
|
||||||
.smallest(Unit::Day)
|
duration -= TimeDelta::weeks(y * 52);
|
||||||
.relative(todate),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
if let Some(cmp) = elapsed.compare((contact.periodicity, todate)).ok() {
|
|
||||||
if cmp == std::cmp::Ordering::Less {
|
|
||||||
return None;
|
|
||||||
}
|
}
|
||||||
|
let w = duration.num_weeks();
|
||||||
|
if w > 0 {
|
||||||
|
elapsed.push(count(w, "week"));
|
||||||
|
duration -= TimeDelta::weeks(w);
|
||||||
|
}
|
||||||
|
let d = duration.num_days();
|
||||||
|
if d > 0 {
|
||||||
|
elapsed.push(count(d, "day"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let elapsed_str = if elapsed.is_zero() {
|
let elapsed_str = if elapsed.is_empty() {
|
||||||
"today".to_string()
|
"today".to_string()
|
||||||
} else {
|
} else {
|
||||||
format!("{:#}", elapsed)
|
elapsed.join(", ")
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(ContactFreshness {
|
ContactFreshness {
|
||||||
contact_id: contact.id,
|
contact_id: contact.id,
|
||||||
display: contact.display_name(),
|
display: contact.display_name(),
|
||||||
fresh_date,
|
fresh_date,
|
||||||
fresh_str: fresh_date.to_string(),
|
fresh_str: fresh_date.to_string(),
|
||||||
elapsed_str,
|
elapsed_str,
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -200,8 +197,8 @@ pub mod get {
|
||||||
Some(KnownBirthdayContact {
|
Some(KnownBirthdayContact {
|
||||||
contact_id: contact.id,
|
contact_id: contact.id,
|
||||||
display: contact.display_name(),
|
display: contact.display_name(),
|
||||||
prev_birthday: date.prev_month_day_occurrence()?,
|
prev_birthday: date.prev_month_day_occurrence().unwrap(),
|
||||||
next_birthday: date.next_month_day_occurrence()?,
|
next_birthday: date.next_month_day_occurrence().unwrap(),
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,9 @@ mod get {
|
||||||
for contact in &contacts {
|
for contact in &contacts {
|
||||||
if let Some(Birthday::Date(yo_date)) = &contact.birthday {
|
if let Some(Birthday::Date(yo_date)) = &contact.birthday {
|
||||||
if let Some(date) = NaiveDate::from_ymd_opt(
|
if let Some(date) = NaiveDate::from_ymd_opt(
|
||||||
yo_date.year.unwrap_or(1900).into(),
|
yo_date.year.unwrap_or(1900),
|
||||||
yo_date.month.try_into().unwrap(),
|
yo_date.month,
|
||||||
yo_date.day.try_into().unwrap(),
|
yo_date.day,
|
||||||
) {
|
) {
|
||||||
calendar.push(
|
calendar.push(
|
||||||
Event::new()
|
Event::new()
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,14 @@ use axum::{
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::{delete, patch, post},
|
routing::{delete, patch, post},
|
||||||
};
|
};
|
||||||
use jiff::{Zoned, civil::Date};
|
use chrono::{Datelike, Local, NaiveDate};
|
||||||
use maud::Markup;
|
use maud::Markup;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::models::JournalEntry;
|
use crate::models::JournalEntry;
|
||||||
use crate::models::user::AuthSession;
|
use crate::models::user::AuthSession;
|
||||||
use crate::switchboard::{MentionHostType, insert_mentions};
|
use crate::switchboard::{MentionHost, MentionHostType, insert_mentions};
|
||||||
use crate::{AppError, AppState, DbId};
|
use crate::{AppError, AppState, DbId};
|
||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
|
|
@ -39,10 +39,10 @@ mod post {
|
||||||
let pool = &state.db(&user).pool;
|
let pool = &state.db(&user).pool;
|
||||||
let sw_lock = state.switchboard(&user);
|
let sw_lock = state.switchboard(&user);
|
||||||
|
|
||||||
let now = Zoned::now();
|
let now = Local::now().date_naive();
|
||||||
|
|
||||||
let date = if payload.date.is_empty() {
|
let date = if payload.date.is_empty() {
|
||||||
now.date()
|
now
|
||||||
} else {
|
} else {
|
||||||
let date_re =
|
let date_re =
|
||||||
Regex::new(r"^(?:(?<year>[0-9]{4})-)?(?:(?<month>[0-9]{2})-)?(?<day>[0-9]{2})$")
|
Regex::new(r"^(?:(?<year>[0-9]{4})-)?(?:(?<month>[0-9]{2})-)?(?<day>[0-9]{2})$")
|
||||||
|
|
@ -54,16 +54,17 @@ mod post {
|
||||||
// unwrapping these parses is safe since it's matching [0-9]{2,4}
|
// unwrapping these parses is safe since it's matching [0-9]{2,4}
|
||||||
let year = caps
|
let year = caps
|
||||||
.name("year")
|
.name("year")
|
||||||
.map(|m| m.as_str().parse::<i16>().unwrap())
|
.map(|m| m.as_str().parse::<i32>().unwrap())
|
||||||
.unwrap_or(now.year());
|
.unwrap_or(now.year());
|
||||||
let month = caps
|
let month = caps
|
||||||
.name("month")
|
.name("month")
|
||||||
.map(|m| m.as_str().parse::<i8>().unwrap())
|
.map(|m| m.as_str().parse::<u32>().unwrap())
|
||||||
.unwrap_or(now.month());
|
.unwrap_or(now.month());
|
||||||
let day = caps.name("day").unwrap().as_str().parse::<i8>().unwrap();
|
let day = caps.name("day").unwrap().as_str().parse::<u32>().unwrap();
|
||||||
|
|
||||||
Date::new(year, month, day)
|
NaiveDate::from_ymd_opt(year, month, day).ok_or(anyhow::Error::msg(
|
||||||
.map_err(|_| anyhow::Error::msg("invalid date: failed NaiveDate construction"))?
|
"invalid date: failed NaiveDate construction",
|
||||||
|
))?
|
||||||
};
|
};
|
||||||
|
|
||||||
// not a macro query, we want to use JournalEntry's custom FromRow
|
// not a macro query, we want to use JournalEntry's custom FromRow
|
||||||
|
|
@ -130,6 +131,7 @@ mod patch {
|
||||||
insert_mentions(&mentions, pool).await?;
|
insert_mentions(&mentions, pool).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Ok(new_entry.to_html(pool).await?)
|
Ok(new_entry.to_html(pool).await?)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue