Added banner options, logo and image upload with validation to appearance settings. Fixes #15
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+61
-15
@@ -4,6 +4,7 @@ use std::fmt::{Display, Formatter};
|
||||
use std::str::FromStr;
|
||||
use chrono::{Local, NaiveDate, NaiveTime, Weekday};
|
||||
use lazy_static::lazy_static;
|
||||
use crate::backend::banner::{is_stored_image_name, BannerOptions};
|
||||
use crate::backend::color::Colors;
|
||||
use regex::Regex;
|
||||
use rust_decimal::Decimal;
|
||||
@@ -746,7 +747,11 @@ pub struct Appearance {
|
||||
// Forms that edit the title or the text don't send the colours
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "ssr", sqlx(flatten))]
|
||||
pub colors: Colors
|
||||
pub colors: Colors,
|
||||
pub logo: Option<String>,
|
||||
#[serde(default)]
|
||||
#[cfg_attr(feature = "ssr", sqlx(flatten))]
|
||||
pub banner_options: BannerOptions
|
||||
}
|
||||
|
||||
impl Appearance {
|
||||
@@ -754,21 +759,46 @@ impl Appearance {
|
||||
self.id
|
||||
}
|
||||
|
||||
/// URL of the banner image, if there is one
|
||||
pub fn banner_url(&self) -> Option<String> {
|
||||
stored_image_url(&self.banner)
|
||||
}
|
||||
|
||||
/// URL of the logo, if there is one
|
||||
pub fn logo_url(&self) -> Option<String> {
|
||||
stored_image_url(&self.logo)
|
||||
}
|
||||
|
||||
/// The public page has a header when there is anything to show in it
|
||||
pub fn has_banner(&self) -> bool {
|
||||
self.banner_url().is_some()
|
||||
|| self.logo_url().is_some()
|
||||
|| !self.title.clone().unwrap_or_default().trim().is_empty()
|
||||
}
|
||||
|
||||
/// CSS variables for the banner of the public page, used as its `style` attribute.
|
||||
/// The styles themselves are in `assets/css/public.css`.
|
||||
pub fn banner_style(&self) -> String {
|
||||
match &self.banner {
|
||||
Some(banner) if is_css_safe_file_name(banner) => format!("--rz-banner-image: url('/data/{}')", banner),
|
||||
_ => String::new()
|
||||
}
|
||||
banner_style(self.banner_url(), &self.banner_options)
|
||||
}
|
||||
}
|
||||
|
||||
/// Style of a banner with the given image; the preview in the appearance settings uses it
|
||||
/// with an image that is not uploaded yet
|
||||
pub fn banner_style(image_url: Option<String>, options: &BannerOptions) -> String {
|
||||
let options = options.style(image_url.is_some());
|
||||
|
||||
match image_url {
|
||||
Some(url) => format!("--rz-banner-image: url('{}'); {}", url, options),
|
||||
None => options
|
||||
}
|
||||
}
|
||||
|
||||
/// The value is put into a `style` attribute, so it must not be able to end the `url()` or the declaration
|
||||
fn is_css_safe_file_name(name: &str) -> bool {
|
||||
!name.is_empty()
|
||||
&& !name.starts_with('.')
|
||||
&& name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '.')
|
||||
fn stored_image_url(name: &Option<String>) -> Option<String> {
|
||||
name.as_ref()
|
||||
.filter(|name| is_stored_image_name(name))
|
||||
.map(|name| format!("/data/{}", name))
|
||||
}
|
||||
|
||||
pub fn validate_closing_time(time: &ClosingTime) -> Result<(), ValidationError> {
|
||||
@@ -1295,22 +1325,38 @@ mod tests {
|
||||
#[test]
|
||||
fn banner_style_sets_image_variable() {
|
||||
let appearance = Appearance { banner: Some("my-banner_1.jpg".to_string()), ..Default::default() };
|
||||
assert_eq!(appearance.banner_style(), "--rz-banner-image: url('/data/my-banner_1.jpg')");
|
||||
assert_eq!(appearance.banner_style(),
|
||||
format!("--rz-banner-image: url('/data/my-banner_1.jpg'); {}", BannerOptions::default().style(true)));
|
||||
assert!(appearance.has_banner());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banner_style_is_empty_without_banner() {
|
||||
assert_eq!(Appearance::default().banner_style(), "");
|
||||
fn banner_style_without_image_has_only_options() {
|
||||
assert_eq!(Appearance::default().banner_style(), BannerOptions::default().style(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banner_style_ignores_unsafe_file_names() {
|
||||
for name in ["", "a'); color: red; x: url('.jpg", "a b.jpg", "../config.toml", "a/b.jpg", "a\\b.jpg", "a\"b.jpg", "a;b.jpg", "a)b.jpg", ".hidden"] {
|
||||
let appearance = Appearance { banner: Some(name.to_string()), ..Default::default() };
|
||||
assert_eq!(appearance.banner_style(), "", "name: {:?}", name);
|
||||
for name in ["", "a'); color: red; x: url('.jpg", "a b.jpg", "../config.toml", "a/b.jpg", ".hidden"] {
|
||||
let appearance = Appearance { banner: Some(name.to_string()), logo: Some(name.to_string()), ..Default::default() };
|
||||
assert_eq!(appearance.banner_style(), BannerOptions::default().style(false), "name: {:?}", name);
|
||||
assert_eq!(appearance.banner_url(), None, "name: {:?}", name);
|
||||
assert_eq!(appearance.logo_url(), None, "name: {:?}", name);
|
||||
assert!(!appearance.has_banner(), "name: {:?}", name);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn banner_is_shown_for_title_logo_or_image() {
|
||||
assert!(!Appearance::default().has_banner());
|
||||
assert!(!Appearance { title: Some(" ".to_string()), ..Default::default() }.has_banner());
|
||||
assert!(Appearance { title: Some("Tennis".to_string()), ..Default::default() }.has_banner());
|
||||
|
||||
let with_logo = Appearance { logo: Some("logo-1a2b3c4d.png".to_string()), ..Default::default() };
|
||||
assert!(with_logo.has_banner());
|
||||
assert_eq!(with_logo.logo_url(), Some("/data/logo-1a2b3c4d.png".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enum_defaults() {
|
||||
assert_eq!(ReservationState::default(), ReservationState::New);
|
||||
|
||||
Reference in New Issue
Block a user