//! Notes and todos on one occurrence of a date: this year's Thanksgiving, next Tuesday's
//! 1:1, Anji's birthday on the 28th
use crate::data::{anniversary_in, CalendarOps};
use crate::entities::prelude::{Note, Ppl, SigDate};
use crate::entities::{note, ppl, sig_date};
use crate::holidays::{self, Holiday};
use crate::PplError;
use chrono::{Datelike, Local, NaiveDate};
use sea_orm::ActiveValue::Set;
use sea_orm::{ActiveModelTrait, DatabaseConnection, DbErr, EntityTrait, NotSet};
use std::io;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Kind {
Note,
Todo,
}
impl Kind {
fn as_str(self) -> &'static str {
match self {
Kind::Note => "note",
Kind::Todo => "todo",
}
}
}
/// What a note hangs on
#[derive(Clone, Debug, PartialEq)]
pub enum Target {
Holiday { calendar: String, event: String },
Person { sig_date_id: i32 },
}
/// One occurrence of a holiday, iCal event or person's date
#[derive(Clone, Debug)]
pub struct Occurrence {
pub date: NaiveDate,
pub target: Target,
/// "Thanksgiving (us)" or "birthday · Anji"
pub title: String,
}
impl Occurrence {
pub fn of_holiday(h: &Holiday) -> Self {
Occurrence {
date: h.date,
target: Target::Holiday {
calendar: h.calendar.key.to_string(),
event: h.name.clone(),
},
title: format!("{} ({})", h.name, h.calendar.key),
}
}
pub fn of_person(date: &sig_date::Model, on: NaiveDate, person: &ppl::Model) -> Self {
Occurrence {
date: on,
target: Target::Person {
sig_date_id: date.id,
},
title: format!(
"{} · {}",
date.event,
person.nick.as_deref().unwrap_or(person.name.trim())
),
}
}
fn matches(&self, n: ¬e::Model) -> bool {
n.date == self.date
&& match &self.target {
Target::Holiday { calendar, event } => {
n.calendar.as_deref() == Some(calendar) && n.event.as_deref() == Some(event)
}
Target::Person { sig_date_id } => n.sig_date_id == Some(*sig_date_id),
}
}
/// Its notes, then its todos, oldest first
pub fn notes<'a>(&self, all: &'a [note::Model]) -> Vec<&'a note::Model> {
let mut mine = all.iter().filter(|n| self.matches(n)).collect::<Vec<_>>();
mine.sort_by_key(|n| (n.kind != "note", n.id));
mine
}
}
/// "📝 bring pie", "☐ buy a card", "✓ book a table"
pub fn line(n: ¬e::Model) -> String {
match (n.kind.as_str(), n.done) {
("todo", true) => format!("✓ {}", n.text),
("todo", false) => format!("☐ {}", n.text),
_ => format!("📝 {}", n.text),
}
}
/// A short mark for an upcoming row with notes: open todos count, else 📝
pub fn badge(notes: &[¬e::Model]) -> Option<String> {
let open = notes.iter().filter(|n| n.kind == "todo" && !n.done).count();
match (open, notes.is_empty()) {
(0, true) => None,
(0, false) => Some("📝".to_string()),
(n, _) => Some(format!("☐{}", n)),
}
}
pub struct NoteOps {}
impl NoteOps {
pub async fn list(db: &DatabaseConnection) -> Result<Vec<note::Model>, DbErr> {
Note::find().all(db).await
}
pub async fn add(
db: &DatabaseConnection,
occ: &Occurrence,
kind: Kind,
text: &str,
) -> Result<note::Model, PplError> {
let text = text.trim();
if text.is_empty() {
return Err(
io::Error::new(io::ErrorKind::InvalidInput, "a note needs some text").into(),
);
}
let today = Local::now().date_naive();
let (calendar, event, sig_date_id) = match &occ.target {
Target::Holiday { calendar, event } => {
(Some(calendar.clone()), Some(event.clone()), None)
}
Target::Person { sig_date_id } => (None, None, Some(*sig_date_id)),
};
Ok(note::ActiveModel {
id: NotSet,
kind: Set(kind.as_str().to_string()),
text: Set(text.to_string()),
done: Set(false),
date: Set(occ.date),
calendar: Set(calendar),
event: Set(event),
sig_date_id: Set(sig_date_id),
date_ins: Set(today),
date_up: Set(today),
}
.insert(db)
.await?)
}
/// Tick a todo, or untick it
pub async fn toggle(db: &DatabaseConnection, id: i32) -> Result<note::Model, PplError> {
let Some(n) = Note::find_by_id(id).one(db).await? else {
return Err(not_found(id));
};
let done = !n.done;
let mut am: note::ActiveModel = n.into();
am.done = Set(done);
am.date_up = Set(Local::now().date_naive());
Ok(am.update(db).await?)
}
pub async fn delete(db: &DatabaseConnection, id: i32) -> Result<(), PplError> {
match Note::delete_by_id(id).exec(db).await?.rows_affected {
0 => Err(not_found(id)),
_ => Ok(()),
}
}
}
fn not_found(id: i32) -> PplError {
io::Error::new(
io::ErrorKind::NotFound,
format!("no note {}; see `ppl notes`", id),
)
.into()
}
/// Every occurrence from `from` through `days` days on: people's dates and the holidays
/// of the calendars that are on
pub async fn upcoming(
db: &DatabaseConnection,
from: NaiveDate,
days: i64,
) -> Result<Vec<Occurrence>, PplError> {
let to = from + chrono::Duration::days(days);
let ppl = Ppl::find().all(db).await?;
let dates = SigDate::find().all(db).await?;
crate::ical::refresh_stale(&CalendarOps::subscriptions(db).await?);
let enabled = CalendarOps::enabled(db).await?;
let mut out = vec![];
for year in from.year()..=to.year() {
for d in &dates {
let (Some(on), Some(p)) = (
anniversary_in(d.date, year),
ppl.iter().find(|p| p.id == d.ppl_id),
) else {
continue;
};
out.push(Occurrence::of_person(d, on, p));
}
out.extend(
holidays::holidays_in(year, &enabled)
.iter()
.map(Occurrence::of_holiday),
);
}
out.retain(|o| o.date >= from && o.date <= to);
out.sort_by_key(|o| o.date);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn n(
id: i32,
kind: &str,
done: bool,
date: NaiveDate,
cal: Option<&str>,
sig: Option<i32>,
) -> note::Model {
note::Model {
id,
kind: kind.to_string(),
text: format!("text {}", id),
done,
date,
calendar: cal.map(|c| c.to_string()),
event: cal.map(|_| "Thanksgiving".to_string()),
sig_date_id: sig,
date_ins: date,
date_up: date,
}
}
#[test]
fn notes_find_their_occurrence() {
let day = NaiveDate::from_ymd_opt(2026, 11, 26).unwrap();
let next = NaiveDate::from_ymd_opt(2027, 11, 25).unwrap();
let all = vec![
n(1, "todo", false, day, Some("us"), None),
n(2, "note", false, day, Some("us"), None),
n(3, "note", false, next, Some("us"), None),
n(4, "note", false, day, Some("ca"), None),
n(5, "todo", true, day, None, Some(7)),
];
let thanksgiving = Occurrence {
date: day,
target: Target::Holiday {
calendar: "us".to_string(),
event: "Thanksgiving".to_string(),
},
title: String::new(),
};
// Only this year's, in this calendar; notes before todos
let ids = thanksgiving
.notes(&all)
.iter()
.map(|n| n.id)
.collect::<Vec<_>>();
assert_eq!(ids, vec![2, 1]);
assert_eq!(badge(&thanksgiving.notes(&all)), Some("☐1".to_string()));
let birthday = Occurrence {
date: day,
target: Target::Person { sig_date_id: 7 },
title: String::new(),
};
assert_eq!(badge(&birthday.notes(&all)), Some("📝".to_string()));
assert_eq!(line(&all[4]), "✓ text 5");
}
}
⭐
Log in to mark this repository.
8.3 KiB · text
History
aea77cc