328 lines
9.1 KiB
Rust
328 lines
9.1 KiB
Rust
/*
|
|
* This file is part of mailpot
|
|
*
|
|
* Copyright 2020 - Manos Pitsidianakis
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU Affero General Public License as
|
|
* published by the Free Software Foundation, either version 3 of the
|
|
* License, or (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU Affero General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Affero General Public License
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
use super::*;
|
|
|
|
lazy_static::lazy_static! {
|
|
pub static ref TEMPLATES: Environment<'static> = {
|
|
let mut env = Environment::new();
|
|
macro_rules! add_function {
|
|
($($id:ident),*$(,)?) => {
|
|
$(env.add_function(stringify!($id), $id);)*
|
|
}
|
|
}
|
|
add_function!(
|
|
calendarize,
|
|
login_path,
|
|
logout_path,
|
|
settings_path,
|
|
help_path,
|
|
list_path,
|
|
list_settings_path,
|
|
list_edit_path,
|
|
list_post_path
|
|
);
|
|
env.set_source(Source::from_path("web/src/templates/"));
|
|
|
|
env
|
|
};
|
|
}
|
|
|
|
pub trait StripCarets {
|
|
fn strip_carets(&self) -> &str;
|
|
}
|
|
|
|
impl StripCarets for &str {
|
|
fn strip_carets(&self) -> &str {
|
|
let mut self_ref = self.trim();
|
|
if self_ref.starts_with('<') && self_ref.ends_with('>') {
|
|
self_ref = &self_ref[1..self_ref.len().saturating_sub(1)];
|
|
}
|
|
self_ref
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct MailingList {
|
|
pub pk: i64,
|
|
pub name: String,
|
|
pub id: String,
|
|
pub address: String,
|
|
pub description: Option<String>,
|
|
pub archive_url: Option<String>,
|
|
pub inner: DbVal<mailpot::models::MailingList>,
|
|
}
|
|
|
|
impl From<DbVal<mailpot::models::MailingList>> for MailingList {
|
|
fn from(val: DbVal<mailpot::models::MailingList>) -> Self {
|
|
let DbVal(
|
|
mailpot::models::MailingList {
|
|
pk,
|
|
name,
|
|
id,
|
|
address,
|
|
description,
|
|
archive_url,
|
|
},
|
|
_,
|
|
) = val.clone();
|
|
|
|
Self {
|
|
pk,
|
|
name,
|
|
id,
|
|
address,
|
|
description,
|
|
archive_url,
|
|
inner: val,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Display for MailingList {
|
|
fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
self.id.fmt(fmt)
|
|
}
|
|
}
|
|
|
|
impl Object for MailingList {
|
|
fn kind(&self) -> minijinja::value::ObjectKind {
|
|
minijinja::value::ObjectKind::Struct(self)
|
|
}
|
|
|
|
fn call_method(
|
|
&self,
|
|
_state: &minijinja::State,
|
|
name: &str,
|
|
_args: &[Value],
|
|
) -> std::result::Result<Value, Error> {
|
|
match name {
|
|
"subscription_mailto" => {
|
|
Ok(Value::from_serializable(&self.inner.subscription_mailto()))
|
|
}
|
|
"unsubscription_mailto" => Ok(Value::from_serializable(
|
|
&self.inner.unsubscription_mailto(),
|
|
)),
|
|
_ => Err(Error::new(
|
|
minijinja::ErrorKind::UnknownMethod,
|
|
format!("object has no method named {name}"),
|
|
)),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl minijinja::value::StructObject for MailingList {
|
|
fn get_field(&self, name: &str) -> Option<Value> {
|
|
match name {
|
|
"pk" => Some(Value::from_serializable(&self.pk)),
|
|
"name" => Some(Value::from_serializable(&self.name)),
|
|
"id" => Some(Value::from_serializable(&self.id)),
|
|
"address" => Some(Value::from_serializable(&self.address)),
|
|
"description" => Some(Value::from_serializable(&self.description)),
|
|
"archive_url" => Some(Value::from_serializable(&self.archive_url)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn static_fields(&self) -> Option<&'static [&'static str]> {
|
|
Some(&["pk", "name", "id", "address", "description", "archive_url"][..])
|
|
}
|
|
}
|
|
|
|
pub fn calendarize(
|
|
_state: &minijinja::State,
|
|
args: Value,
|
|
hists: Value,
|
|
) -> std::result::Result<Value, Error> {
|
|
use chrono::Month;
|
|
|
|
macro_rules! month {
|
|
($int:expr) => {{
|
|
let int = $int;
|
|
match int {
|
|
1 => Month::January.name(),
|
|
2 => Month::February.name(),
|
|
3 => Month::March.name(),
|
|
4 => Month::April.name(),
|
|
5 => Month::May.name(),
|
|
6 => Month::June.name(),
|
|
7 => Month::July.name(),
|
|
8 => Month::August.name(),
|
|
9 => Month::September.name(),
|
|
10 => Month::October.name(),
|
|
11 => Month::November.name(),
|
|
12 => Month::December.name(),
|
|
_ => unreachable!(),
|
|
}
|
|
}};
|
|
}
|
|
let month = args.as_str().unwrap();
|
|
let hist = hists
|
|
.get_item(&Value::from(month))?
|
|
.as_seq()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|v| usize::try_from(v).unwrap())
|
|
.collect::<Vec<usize>>();
|
|
let sum: usize = hists
|
|
.get_item(&Value::from(month))?
|
|
.as_seq()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|v| usize::try_from(v).unwrap())
|
|
.sum();
|
|
let date = chrono::NaiveDate::parse_from_str(&format!("{}-01", month), "%F").unwrap();
|
|
// Week = [Mon, Tue, Wed, Thu, Fri, Sat, Sun]
|
|
Ok(minijinja::context! {
|
|
month_name => month!(date.month()),
|
|
month => month,
|
|
month_int => date.month() as usize,
|
|
year => date.year(),
|
|
weeks => cal::calendarize_with_offset(date, 1),
|
|
hist => hist,
|
|
sum => sum,
|
|
})
|
|
}
|
|
|
|
#[derive(Debug, PartialEq, Eq, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct Crumb {
|
|
pub label: Cow<'static, str>,
|
|
pub url: Cow<'static, str>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Hash, Copy, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub enum Level {
|
|
Success,
|
|
#[default]
|
|
Info,
|
|
Warning,
|
|
Error,
|
|
}
|
|
|
|
#[derive(Debug, Hash, Clone, serde::Deserialize, serde::Serialize)]
|
|
pub struct Message {
|
|
pub message: Cow<'static, str>,
|
|
#[serde(default)]
|
|
pub level: Level,
|
|
}
|
|
|
|
impl Message {
|
|
const MESSAGE_KEY: &str = "session-message";
|
|
}
|
|
|
|
pub trait SessionMessages {
|
|
fn drain_messages(&mut self) -> Vec<Message>;
|
|
fn add_message(&mut self, _: Message) -> Result<(), ResponseError>;
|
|
}
|
|
|
|
impl SessionMessages for WritableSession {
|
|
fn drain_messages(&mut self) -> Vec<Message> {
|
|
let ret = self.get(Message::MESSAGE_KEY).unwrap_or_default();
|
|
self.remove(Message::MESSAGE_KEY);
|
|
ret
|
|
}
|
|
|
|
fn add_message(&mut self, message: Message) -> Result<(), ResponseError> {
|
|
let mut messages: Vec<Message> = self.get(Message::MESSAGE_KEY).unwrap_or_default();
|
|
messages.push(message);
|
|
self.insert(Message::MESSAGE_KEY, messages)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)]
|
|
#[repr(transparent)]
|
|
pub struct IntPOST(pub i64);
|
|
|
|
impl serde::Serialize for IntPOST {
|
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
|
where
|
|
S: serde::Serializer,
|
|
{
|
|
serializer.serialize_i64(self.0)
|
|
}
|
|
}
|
|
|
|
impl<'de> serde::Deserialize<'de> for IntPOST {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
struct IntVisitor;
|
|
|
|
impl<'de> serde::de::Visitor<'de> for IntVisitor {
|
|
type Value = IntPOST;
|
|
|
|
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
f.write_str("Int as a number or string")
|
|
}
|
|
|
|
fn visit_i64<E>(self, int: i64) -> Result<Self::Value, E>
|
|
where
|
|
E: serde::de::Error,
|
|
{
|
|
Ok(IntPOST(int))
|
|
}
|
|
|
|
fn visit_str<E>(self, int: &str) -> Result<Self::Value, E>
|
|
where
|
|
E: serde::de::Error,
|
|
{
|
|
int.parse().map(IntPOST).map_err(serde::de::Error::custom)
|
|
}
|
|
}
|
|
|
|
deserializer.deserialize_any(IntVisitor)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, serde::Deserialize)]
|
|
pub struct Next {
|
|
#[serde(default, deserialize_with = "empty_string_as_none")]
|
|
pub next: Option<String>,
|
|
}
|
|
|
|
impl Next {
|
|
#[inline]
|
|
pub fn or_else(self, cl: impl FnOnce() -> String) -> Redirect {
|
|
if let Some(next) = self.next {
|
|
Redirect::to(&next)
|
|
} else {
|
|
Redirect::to(&cl())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Serde deserialization decorator to map empty Strings to None,
|
|
fn empty_string_as_none<'de, D, T>(de: D) -> Result<Option<T>, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
T: std::str::FromStr,
|
|
T::Err: std::fmt::Display,
|
|
{
|
|
use serde::Deserialize;
|
|
let opt = Option::<String>::deserialize(de)?;
|
|
match opt.as_deref() {
|
|
None | Some("") => Ok(None),
|
|
Some(s) => std::str::FromStr::from_str(s)
|
|
.map_err(serde::de::Error::custom)
|
|
.map(Some),
|
|
}
|
|
}
|