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"
|
||||
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]]
|
||||
name = "js-sys"
|
||||
version = "0.3.81"
|
||||
|
|
@ -1468,7 +1427,6 @@ dependencies = [
|
|||
"http",
|
||||
"icalendar",
|
||||
"itertools 0.14.0",
|
||||
"jiff",
|
||||
"listenfd",
|
||||
"markdown",
|
||||
"maud",
|
||||
|
|
@ -1889,21 +1847,6 @@ version = "0.3.32"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "potential_utf"
|
||||
version = "0.1.3"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ clap = { version = "4.5.53", features = ["derive"] }
|
|||
http = "1.3.1"
|
||||
icalendar = "0.17.5"
|
||||
itertools = "0.14.0"
|
||||
jiff = { version = "0.2.23", features = ["serde"] }
|
||||
listenfd = "1.0.2"
|
||||
markdown = "1.0.0"
|
||||
maud = { version = "0.27.0", features = ["axum"] }
|
||||
|
|
|
|||
|
|
@ -30,14 +30,14 @@ test.skip("groups wrap nicely", async ({ page }) => {
|
|||
|
||||
const groupBox = page.getByPlaceholder(/group name/i);
|
||||
await groupBox.fill('this is a long group name');
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
await page.getByRole('button', { name: /save/i }).click();
|
||||
await expect(page.locator('#alpine-loaded')).not.toHaveAttribute('x-cloak');
|
||||
|
||||
// TODO: this drives to the right location but i can't figure out how to assert
|
||||
// that the text is all on one line. Manual inspection looks good at time of writing.
|
||||
// TODO: this drives to the right location but i can't figure out how to assert
|
||||
// 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 }) => {
|
||||
|
||||
});
|
||||
|
||||
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: clicking off contact list closes it
|
||||
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::{FromRow, Row};
|
||||
use std::fmt::Display;
|
||||
|
|
@ -29,31 +29,25 @@ impl Display for Birthday {
|
|||
}
|
||||
|
||||
impl Birthday {
|
||||
pub fn next_occurrence(&self) -> Option<civil::Date> {
|
||||
pub fn next_occurrence(&self) -> Option<chrono::NaiveDate> {
|
||||
match &self {
|
||||
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()
|
||||
.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
|
||||
pub fn age(&self) -> Option<i32> {
|
||||
pub fn age(&self) -> Option<u32> {
|
||||
match &self {
|
||||
Birthday::Text(_) => None,
|
||||
Birthday::Date(date) => {
|
||||
let now = Timestamp::now().to_zoned(TimeZone::UTC);
|
||||
date.to_civil_date().map(|birthdate| {
|
||||
now.since(&birthdate.to_zoned(TimeZone::UTC).unwrap())
|
||||
.unwrap()
|
||||
.total((Unit::Year, &now))
|
||||
.unwrap() as i32
|
||||
})
|
||||
}
|
||||
Birthday::Date(date) => date
|
||||
.to_date_naive()
|
||||
.map(|birthdate| Local::now().date_naive().years_since(birthdate))?,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use jiff::{Span, Timestamp, civil::Date};
|
||||
use chrono::{DateTime, NaiveDate, Utc};
|
||||
use sqlx::sqlite::SqlitePool;
|
||||
use std::str::FromStr;
|
||||
|
||||
|
|
@ -12,20 +12,14 @@ struct RawContact {
|
|||
birthday: Option<String>,
|
||||
manually_freshened_at: Option<String>,
|
||||
lives_with: String,
|
||||
can_stale: bool,
|
||||
active: bool,
|
||||
periodicity: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Contact {
|
||||
pub id: DbId,
|
||||
pub birthday: Option<Birthday>,
|
||||
pub manually_freshened_at: Option<Timestamp>,
|
||||
pub manually_freshened_at: Option<DateTime<Utc>>,
|
||||
pub lives_with: String,
|
||||
pub can_stale: bool,
|
||||
pub active: bool,
|
||||
pub periodicity: Span,
|
||||
}
|
||||
|
||||
impl Into<Contact> for RawContact {
|
||||
|
|
@ -37,11 +31,9 @@ impl Into<Contact> for RawContact {
|
|||
.and_then(|s| Birthday::from_str(s.as_ref()).ok()),
|
||||
manually_freshened_at: self
|
||||
.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,
|
||||
can_stale: self.can_stale,
|
||||
active: self.active,
|
||||
periodicity: self.periodicity.parse().unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -51,10 +43,6 @@ struct RawHydratedContact {
|
|||
birthday: Option<String>,
|
||||
manually_freshened_at: Option<String>,
|
||||
lives_with: String,
|
||||
can_stale: bool,
|
||||
active: bool,
|
||||
periodicity: String,
|
||||
|
||||
last_mention_date: Option<String>,
|
||||
names: Option<String>,
|
||||
}
|
||||
|
|
@ -62,7 +50,7 @@ struct RawHydratedContact {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct HydratedContact {
|
||||
pub contact: Contact,
|
||||
pub last_mention_date: Option<Date>,
|
||||
pub last_mention_date: Option<NaiveDate>,
|
||||
pub names: Vec<String>,
|
||||
}
|
||||
|
||||
|
|
@ -74,9 +62,6 @@ impl Into<HydratedContact> for RawHydratedContact {
|
|||
birthday: self.birthday,
|
||||
manually_freshened_at: self.manually_freshened_at,
|
||||
lives_with: self.lives_with,
|
||||
can_stale: self.can_stale,
|
||||
active: self.active,
|
||||
periodicity: self.periodicity,
|
||||
}),
|
||||
names: self
|
||||
.names
|
||||
|
|
@ -86,7 +71,7 @@ impl Into<HydratedContact> for RawHydratedContact {
|
|||
.collect::<Vec<String>>(),
|
||||
last_mention_date: self
|
||||
.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> {
|
||||
// copy-paste the query from 'all', then add "where c.id = $2" to the last line
|
||||
let raw = sqlx::query_as!(
|
||||
RawHydratedContact,
|
||||
r#"select
|
||||
id,
|
||||
birthday,
|
||||
lives_with,
|
||||
manually_freshened_at as "manually_freshened_at: String",
|
||||
can_stale,
|
||||
active,
|
||||
periodicity,
|
||||
(
|
||||
r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", (
|
||||
select string_agg(name,x'1c' order by sort)
|
||||
from names where contact_id = c.id
|
||||
) as names, (
|
||||
|
|
@ -154,15 +123,7 @@ impl HydratedContact {
|
|||
pub async fn all(pool: &SqlitePool) -> Result<Vec<Self>, AppError> {
|
||||
let contacts = sqlx::query_as!(
|
||||
RawHydratedContact,
|
||||
r#"select
|
||||
id,
|
||||
birthday,
|
||||
lives_with,
|
||||
manually_freshened_at as "manually_freshened_at: String",
|
||||
can_stale,
|
||||
active,
|
||||
periodicity,
|
||||
(
|
||||
r#"select id, birthday, lives_with, manually_freshened_at as "manually_freshened_at: String", (
|
||||
select string_agg(name,x'1c' order by sort)
|
||||
from names where contact_id = c.id
|
||||
) as names, (
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use jiff::civil::Date;
|
||||
use chrono::NaiveDate;
|
||||
use maud::{Markup, html};
|
||||
use serde_json::json;
|
||||
use sqlx::sqlite::{SqlitePool, SqliteRow};
|
||||
|
|
@ -12,7 +12,7 @@ use crate::switchboard::{MentionHost, MentionHostType};
|
|||
pub struct JournalEntry {
|
||||
pub id: DbId,
|
||||
pub value: String,
|
||||
pub date: Date,
|
||||
pub date: NaiveDate,
|
||||
}
|
||||
|
||||
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 value: String = row.try_get("value")?;
|
||||
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 })
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
use jiff::{Timestamp, civil::Date, tz::TimeZone};
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use regex::Regex;
|
||||
use sqlx::{Database, Decode, Encode, Sqlite, encode::IsNull};
|
||||
use std::fmt::Display;
|
||||
|
|
@ -6,39 +6,38 @@ use std::str::FromStr;
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct YearOptionalDate {
|
||||
pub year: Option<i16>,
|
||||
pub month: i8,
|
||||
pub day: i8,
|
||||
pub year: Option<i32>,
|
||||
pub month: u32,
|
||||
pub day: u32,
|
||||
}
|
||||
|
||||
impl YearOptionalDate {
|
||||
pub fn prev_month_day_occurrence(&self) -> Option<Date> {
|
||||
let now = Timestamp::now().to_zoned(TimeZone::UTC);
|
||||
pub fn prev_month_day_occurrence(&self) -> Option<NaiveDate> {
|
||||
let now = Local::now();
|
||||
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)
|
||||
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<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> {
|
||||
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> {
|
||||
pub fn to_date_naive(&self) -> Option<NaiveDate> {
|
||||
if let Some(year) = self.year {
|
||||
Date::new(year, self.month, self.day).ok()
|
||||
NaiveDate::from_ymd_opt(year, self.month, self.day)
|
||||
} else {
|
||||
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();
|
||||
if let Some(caps) = date_re.captures(str) {
|
||||
let year_str = &caps[1];
|
||||
let month = i8::from_str(&caps[2]).unwrap();
|
||||
let day = i8::from_str(&caps[3]).unwrap();
|
||||
let month = u32::from_str(&caps[2]).unwrap();
|
||||
let day = u32::from_str(&caps[3]).unwrap();
|
||||
let year = if year_str == "--" {
|
||||
None
|
||||
} else {
|
||||
Some(i16::from_str(year_str).unwrap())
|
||||
Some(i32::from_str(year_str).unwrap())
|
||||
};
|
||||
|
||||
return Ok(Self { year, month, day });
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ impl MentionHost<'_> {
|
|||
}
|
||||
|
||||
impl Switchboard {
|
||||
pub async fn gen_trie(pool: &SqlitePool) -> Result<radix_trie::Trie<String, String>, AppError> {
|
||||
pub async fn gen_trie(pool: &SqlitePool) -> Result<radix_trie::Trie<String,String>, AppError> {
|
||||
let mut trie = radix_trie::Trie::new();
|
||||
|
||||
let mentionables = sqlx::query_as!(
|
||||
|
|
@ -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> {
|
||||
let host: MentionHost = host.into();
|
||||
let name_re = Regex::new(r"\[\[(.+?)\]\]").unwrap();
|
||||
|
|
|
|||
|
|
@ -7,19 +7,19 @@ use axum::{
|
|||
};
|
||||
use axum_extra::extract::Form;
|
||||
use cache_bust::asset;
|
||||
use jiff::{Timestamp, Unit, tz::TimeZone};
|
||||
use chrono::DateTime;
|
||||
use maud::{Markup, html};
|
||||
use serde::Deserialize;
|
||||
use serde_json::json;
|
||||
use slug::slugify;
|
||||
use sqlx::QueryBuilder;
|
||||
use sqlx::{QueryBuilder, Sqlite};
|
||||
|
||||
use super::Layout;
|
||||
use super::home::journal_section;
|
||||
use crate::db::DbId;
|
||||
use crate::models::user::AuthSession;
|
||||
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};
|
||||
|
||||
pub mod fields;
|
||||
|
|
@ -40,22 +40,22 @@ pub fn router() -> Router<AppState> {
|
|||
.route("/contact/{contact_id}/edit", get(self::get::contact_edit))
|
||||
}
|
||||
|
||||
fn human_delta(span: &jiff::Span) -> String {
|
||||
let todate = Timestamp::now().to_zoned(TimeZone::UTC).date();
|
||||
let span = span
|
||||
.round(
|
||||
jiff::SpanRound::new()
|
||||
.largest(Unit::Year)
|
||||
.smallest(Unit::Day)
|
||||
.relative(todate),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if span.is_zero() {
|
||||
"today".to_string()
|
||||
} else {
|
||||
format!("in {:#}", span)
|
||||
fn human_delta(delta: &chrono::TimeDelta) -> String {
|
||||
if delta.num_days() == 0 {
|
||||
return "today".to_string();
|
||||
}
|
||||
|
||||
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 {
|
||||
|
|
@ -88,9 +88,7 @@ mod get {
|
|||
.await?;
|
||||
|
||||
let freshened = std::cmp::max(
|
||||
contact
|
||||
.manually_freshened_at
|
||||
.map(|when| when.to_zoned(jiff::tz::TimeZone::UTC).date()),
|
||||
contact.manually_freshened_at.map(|when| when.date_naive()),
|
||||
entries.get(0).map(|entry| entry.date),
|
||||
);
|
||||
|
||||
|
|
@ -132,14 +130,6 @@ mod get {
|
|||
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 {
|
||||
label { "birthday" }
|
||||
div {
|
||||
|
|
@ -223,7 +213,7 @@ mod get {
|
|||
let mfresh_str = contact
|
||||
.manually_freshened_at
|
||||
.clone()
|
||||
.map_or("".to_string(), |m| m.to_string());
|
||||
.map_or("".to_string(), |m| m.to_rfc3339());
|
||||
|
||||
let text_body: String =
|
||||
sqlx::query!("select text_body from contacts where id = $1", contact_id)
|
||||
|
|
@ -243,7 +233,7 @@ mod get {
|
|||
div #error;
|
||||
}
|
||||
|
||||
#fields x-data=(json!({ "status": contact.status() })){
|
||||
div #fields {
|
||||
label { @if contact.names.len() > 1 { "names" } @else { "name" }}
|
||||
div #names x-data=(format!("{{ names: {:?}, new_name: '' }}", &contact.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 = ''";
|
||||
}
|
||||
}
|
||||
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" }
|
||||
div {
|
||||
input name="birthday" value=(contact.birthday.clone().map_or("".to_string(), |b| b.serialize()));
|
||||
|
|
@ -347,8 +324,6 @@ mod put {
|
|||
#[derive(Deserialize)]
|
||||
pub struct PutContact {
|
||||
name: Option<Vec<String>>,
|
||||
status: String,
|
||||
periodicity: Option<String>,
|
||||
birthday: String,
|
||||
manually_freshened_at: String,
|
||||
lives_with: String,
|
||||
|
|
@ -376,22 +351,17 @@ mod put {
|
|||
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
|
||||
} else {
|
||||
Some(
|
||||
payload
|
||||
.manually_freshened_at
|
||||
.parse::<Timestamp>()
|
||||
DateTime::parse_from_str(&payload.manually_freshened_at, "%+")
|
||||
.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() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -404,24 +374,21 @@ mod put {
|
|||
|
||||
sqlx::query!(
|
||||
"update contacts set
|
||||
(
|
||||
birthday, manually_freshened_at, lives_with, text_body,
|
||||
active, can_stale, periodicity
|
||||
) =
|
||||
(?, ?, ?, ?, ?, ?, ?)
|
||||
where id = ?",
|
||||
(birthday, manually_freshened_at, lives_with, text_body) =
|
||||
($1, $2, $3, $4)
|
||||
where id = $5",
|
||||
birthday,
|
||||
manually_freshened_at,
|
||||
payload.lives_with,
|
||||
text_body,
|
||||
active,
|
||||
can_stale,
|
||||
periodicity,
|
||||
contact_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
if old_contact.text_body != text_body {
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
|
|
@ -521,21 +488,25 @@ mod put {
|
|||
let old_names: Vec<String> = old_names.into_iter().map(|(s,)| s).collect();
|
||||
|
||||
if old_names != new_names {
|
||||
sqlx::query!("delete from names where contact_id = $1", contact_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"delete from names where contact_id = $1",
|
||||
contact_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
if !new_names.is_empty() {
|
||||
QueryBuilder::new("insert into names (contact_id, sort, name) ")
|
||||
.push_values(new_names.iter().enumerate(), |mut b, (sort, name)| {
|
||||
b.push_bind(contact_id)
|
||||
.push_bind(DbId::try_from(sort).unwrap())
|
||||
.push_bind(name);
|
||||
})
|
||||
.build()
|
||||
.persistent(false)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
QueryBuilder::new(
|
||||
"insert into names (contact_id, sort, name) "
|
||||
).push_values(new_names.iter().enumerate(), |mut b, (sort, name)| {
|
||||
b
|
||||
.push_bind(contact_id)
|
||||
.push_bind(DbId::try_from(sort).unwrap())
|
||||
.push_bind(name);
|
||||
}).build()
|
||||
.persistent(false)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -553,9 +524,12 @@ mod put {
|
|||
let old_groups: Vec<String> = old_groups.into_iter().map(|(s,)| s).collect();
|
||||
|
||||
if new_groups != old_groups {
|
||||
sqlx::query!("delete from groups where contact_id = $1", contact_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
sqlx::query!(
|
||||
"delete from groups where contact_id = $1",
|
||||
contact_id
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
if new_groups.len() > 0 {
|
||||
QueryBuilder::new("insert into groups (contact_id, name, slug) ")
|
||||
|
|
@ -592,6 +566,7 @@ mod put {
|
|||
.await?;
|
||||
}
|
||||
|
||||
|
||||
if regen_text_body {
|
||||
sqlx::query!(
|
||||
"delete from mentions where entity_id = $1 and entity_type = $2",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use axum::extract::State;
|
||||
use axum::response::IntoResponse;
|
||||
use cache_bust::asset;
|
||||
use jiff::{Timestamp, Unit, Zoned, civil, tz::TimeZone};
|
||||
use chrono::{Local, NaiveDate, TimeDelta};
|
||||
use maud::{Markup, html};
|
||||
use sqlx::sqlite::SqlitePool;
|
||||
|
||||
|
|
@ -15,7 +15,7 @@ use crate::{AppError, AppState};
|
|||
struct ContactFreshness {
|
||||
contact_id: DbId,
|
||||
display: String,
|
||||
fresh_date: civil::Date,
|
||||
fresh_date: NaiveDate,
|
||||
fresh_str: String,
|
||||
elapsed_str: String,
|
||||
}
|
||||
|
|
@ -46,8 +46,8 @@ fn freshness_section(freshens: &Vec<ContactFreshness>) -> Result<Markup, AppErro
|
|||
struct KnownBirthdayContact {
|
||||
contact_id: i64,
|
||||
display: String,
|
||||
prev_birthday: civil::Date,
|
||||
next_birthday: civil::Date,
|
||||
prev_birthday: NaiveDate,
|
||||
next_birthday: NaiveDate,
|
||||
}
|
||||
fn birthdays_section(
|
||||
prev_birthdays: &Vec<KnownBirthdayContact>,
|
||||
|
|
@ -64,7 +64,7 @@ fn birthdays_section(
|
|||
(contact.display)
|
||||
}
|
||||
span {
|
||||
(contact.next_birthday.strftime("%m-%d"))
|
||||
(contact.next_birthday.format("%m-%d"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -75,7 +75,7 @@ fn birthdays_section(
|
|||
(contact.display)
|
||||
}
|
||||
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."
|
||||
}
|
||||
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 {}
|
||||
input type="submit" value="Add Entry";
|
||||
}
|
||||
|
|
@ -134,60 +134,57 @@ pub mod get {
|
|||
let mut freshens: Vec<ContactFreshness> = contacts
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(|contact| {
|
||||
if !contact.can_stale || !contact.active {
|
||||
return None;
|
||||
}
|
||||
|
||||
let zero = jiff::civil::Date::ZERO;
|
||||
.map(|contact| {
|
||||
let zero = NaiveDate::from_epoch_days(0).unwrap();
|
||||
let fresh_date = std::cmp::max(
|
||||
contact
|
||||
.manually_freshened_at
|
||||
.map(|ts| ts.to_zoned(TimeZone::UTC).date())
|
||||
.map(|x| x.date_naive())
|
||||
.unwrap_or(zero),
|
||||
contact.last_mention_date.unwrap_or(zero),
|
||||
);
|
||||
if fresh_date == zero {
|
||||
Some(ContactFreshness {
|
||||
ContactFreshness {
|
||||
contact_id: contact.id,
|
||||
display: contact.display_name(),
|
||||
fresh_date,
|
||||
fresh_str: "never".to_string(),
|
||||
elapsed_str: "".to_string(),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
let utc = TimeZone::UTC;
|
||||
let todate = Timestamp::now().to_zoned(utc.clone()).date();
|
||||
let elapsed = todate
|
||||
.since(&fresh_date.to_zoned(utc).unwrap())
|
||||
.unwrap()
|
||||
.round(
|
||||
jiff::SpanRound::new()
|
||||
.largest(Unit::Year)
|
||||
.smallest(Unit::Day)
|
||||
.relative(todate),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if let Some(cmp) = elapsed.compare((contact.periodicity, todate)).ok() {
|
||||
if cmp == std::cmp::Ordering::Less {
|
||||
return None;
|
||||
}
|
||||
let mut duration = Local::now().date_naive().signed_duration_since(fresh_date);
|
||||
let mut elapsed: Vec<String> = Vec::new();
|
||||
let y = duration.num_weeks() / 52;
|
||||
let count = |n: i64, noun: &str| {
|
||||
format!("{} {}{}", n, noun, if n > 1 { "s" } else { "" })
|
||||
};
|
||||
if y > 0 {
|
||||
elapsed.push(count(y, "year"));
|
||||
duration -= TimeDelta::weeks(y * 52);
|
||||
}
|
||||
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()
|
||||
} else {
|
||||
format!("{:#}", elapsed)
|
||||
elapsed.join(", ")
|
||||
};
|
||||
|
||||
Some(ContactFreshness {
|
||||
ContactFreshness {
|
||||
contact_id: contact.id,
|
||||
display: contact.display_name(),
|
||||
fresh_date,
|
||||
fresh_str: fresh_date.to_string(),
|
||||
elapsed_str,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
|
@ -200,8 +197,8 @@ pub mod get {
|
|||
Some(KnownBirthdayContact {
|
||||
contact_id: contact.id,
|
||||
display: contact.display_name(),
|
||||
prev_birthday: date.prev_month_day_occurrence()?,
|
||||
next_birthday: date.next_month_day_occurrence()?,
|
||||
prev_birthday: date.prev_month_day_occurrence().unwrap(),
|
||||
next_birthday: date.next_month_day_occurrence().unwrap(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
|
|
|||
|
|
@ -61,9 +61,9 @@ mod get {
|
|||
for contact in &contacts {
|
||||
if let Some(Birthday::Date(yo_date)) = &contact.birthday {
|
||||
if let Some(date) = NaiveDate::from_ymd_opt(
|
||||
yo_date.year.unwrap_or(1900).into(),
|
||||
yo_date.month.try_into().unwrap(),
|
||||
yo_date.day.try_into().unwrap(),
|
||||
yo_date.year.unwrap_or(1900),
|
||||
yo_date.month,
|
||||
yo_date.day,
|
||||
) {
|
||||
calendar.push(
|
||||
Event::new()
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ use axum::{
|
|||
response::IntoResponse,
|
||||
routing::{delete, patch, post},
|
||||
};
|
||||
use jiff::{Zoned, civil::Date};
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
use maud::Markup;
|
||||
use regex::Regex;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::models::JournalEntry;
|
||||
use crate::models::user::AuthSession;
|
||||
use crate::switchboard::{MentionHostType, insert_mentions};
|
||||
use crate::switchboard::{MentionHost, MentionHostType, insert_mentions};
|
||||
use crate::{AppError, AppState, DbId};
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
|
|
@ -39,10 +39,10 @@ mod post {
|
|||
let pool = &state.db(&user).pool;
|
||||
let sw_lock = state.switchboard(&user);
|
||||
|
||||
let now = Zoned::now();
|
||||
let now = Local::now().date_naive();
|
||||
|
||||
let date = if payload.date.is_empty() {
|
||||
now.date()
|
||||
now
|
||||
} else {
|
||||
let date_re =
|
||||
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}
|
||||
let year = caps
|
||||
.name("year")
|
||||
.map(|m| m.as_str().parse::<i16>().unwrap())
|
||||
.map(|m| m.as_str().parse::<i32>().unwrap())
|
||||
.unwrap_or(now.year());
|
||||
let month = caps
|
||||
.name("month")
|
||||
.map(|m| m.as_str().parse::<i8>().unwrap())
|
||||
.map(|m| m.as_str().parse::<u32>().unwrap())
|
||||
.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)
|
||||
.map_err(|_| anyhow::Error::msg("invalid date: failed NaiveDate construction"))?
|
||||
NaiveDate::from_ymd_opt(year, month, day).ok_or(anyhow::Error::msg(
|
||||
"invalid date: failed NaiveDate construction",
|
||||
))?
|
||||
};
|
||||
|
||||
// not a macro query, we want to use JournalEntry's custom FromRow
|
||||
|
|
@ -130,6 +131,7 @@ mod patch {
|
|||
insert_mentions(&mentions, pool).await?;
|
||||
}
|
||||
|
||||
|
||||
Ok(new_entry.to_html(pool).await?)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue