refactor: update rating validation logic — enforce minimum rating value
All checks were successful
CI / Check / Test (push) Successful in 1h5m56s

This commit is contained in:
2026-07-10 13:22:21 +02:00
parent fa881c3fd1
commit 6bf4ffc4ab
4 changed files with 54 additions and 4 deletions

View File

@@ -4,10 +4,58 @@ A personal movie diary that tracks what you watch, when, and what you thought ab
## Language ## Language
**Movie**:
A film in the catalog, identified by title and release year. Optionally linked to an external metadata provider (e.g. TMDb) for enrichment. One Movie record is shared across all users — "Blade Runner (1982)" exists once regardless of how many people review it.
_Avoid_: Film entry, title record
**Person**:
Someone involved in making a movie — actor, director, crew member. Sourced from an external metadata provider and enriched with biographical data. Linked to Movies through cast/crew credits. Not a User — Person is movie-industry people only.
_Avoid_: Celebrity, artist, talent
**Review**: **Review**:
A single record of watching a movie — captures the rating, optional comment, when it was watched, and how it was watched. A single record of watching a movie — captures the rating, optional comment, when it was watched, and how it was watched.
_Avoid_: Diary entry, watch, log entry _Avoid_: Diary entry, watch, log entry
**Rating**:
A 15 whole-star score given to a movie in a Review. No half-stars, no zero.
_Avoid_: Score, grade, stars (as a noun for the value itself)
**WatchMedium**: **WatchMedium**:
The channel through which a movie was watched: Cinema, Streaming, TV, PhysicalMedia, Download, MediaServer, or Other. The channel through which a movie was watched: Cinema, Streaming, TV, PhysicalMedia, Download, MediaServer, or Other.
_Avoid_: Source, format, venue, platform _Avoid_: Source, format, venue, platform
**Watchlist**:
A user's collection of movies they intend to watch. Each item is a simple bookmark — no priority or ordering. A movie leaves the watchlist implicitly when reviewed, or explicitly when removed.
_Avoid_: Queue, backlog, to-watch list
**Goal**:
A yearly target a user sets — e.g. "watch 50 movies in 2025." Progress is tracked automatically as reviews are logged. Currently only supports movie-count goals, but the model is designed for other goal types in the future.
_Avoid_: Challenge, resolution, target
**WrapUp**:
A generated summary report of viewing activity over a date range — statistics, trends, highlights, top directors/actors/genres. Can be personal (one user) or global (all users). Generated asynchronously. Shown to users as "Year in Review."
_Avoid_: Stats page, recap, summary
**User**:
A registered account with a username, email, and profile (display name, bio, avatar, banner). Can be Standard or Admin.
_Avoid_: Account, member, profile (as a synonym for the whole User)
**Follow**:
A social relationship where one user subscribes to another's activity. Always requires acceptance by the target user. Works identically for local and federated (ActivityPub) users. Once accepted, the followed user's reviews appear in the follower's Feed.
_Avoid_: Subscribe, connect, friend
**Feed**:
A chronological timeline of reviews from users you follow — both local and federated. The main social surface of the app.
_Avoid_: Timeline, activity stream, home
**WatchEvent**:
An automatically detected viewing reported by an external source — currently Jellyfin and Plex via webhook, but conceptually any system that can report "this person watched this movie" (e.g. a cinema ticket service). Arrives in a pending state; the user confirms it (creating a Review) or dismisses it.
_Avoid_: Playback event, webhook event, auto-import
**Import**:
Bulk ingestion of reviews from an external file — Letterboxd CSV, IMDb CSV, or a generic JSON format. The user uploads a file, column mappings are applied, and reviews are created in batch.
_Avoid_: Upload, migration, sync
**ImportProfile**:
A saved set of column-to-field mappings for an Import. Reusable across imports and shareable between users.
_Avoid_: Template, mapping preset, import config

View File

@@ -104,12 +104,12 @@ pub fn review_to_ap_object(review: &Review, input: ReviewApInput) -> ReviewObjec
let tag = vec![ let tag = vec![
ApHashtag { ApHashtag {
kind: "Hashtag".to_string(), kind: "Hashtag".to_string(),
href: Url::parse(&format!("{}/tags/moviesdiary", &base_url)).expect("valid base_url"), href: Url::parse(&format!("{}/tags/moviesdiary", base_url)).expect("valid base_url"),
name: "#MoviesDiary".to_string(), name: "#MoviesDiary".to_string(),
}, },
ApHashtag { ApHashtag {
kind: "Hashtag".to_string(), kind: "Hashtag".to_string(),
href: Url::parse(&format!("{}/tags/{}", &base_url, normalized.to_lowercase())) href: Url::parse(&format!("{}/tags/{}", base_url, normalized.to_lowercase()))
.expect("valid base_url"), .expect("valid base_url"),
name: format!("#{}", normalized), name: format!("#{}", normalized),
}, },

View File

@@ -9,13 +9,14 @@ fn movie_id_generate_unique() {
#[test] #[test]
fn rating_valid_range() { fn rating_valid_range() {
assert!(Rating::new(0).is_ok()); assert!(Rating::new(1).is_ok());
assert!(Rating::new(5).is_ok()); assert!(Rating::new(5).is_ok());
assert_eq!(Rating::new(3).unwrap().value(), 3); assert_eq!(Rating::new(3).unwrap().value(), 3);
} }
#[test] #[test]
fn rating_invalid() { fn rating_invalid() {
assert!(Rating::new(0).is_err());
assert!(Rating::new(6).is_err()); assert!(Rating::new(6).is_err());
assert!(Rating::new(255).is_err()); assert!(Rating::new(255).is_err());
} }

View File

@@ -86,10 +86,11 @@ pub fn format_watched_at(dt: &chrono::NaiveDateTime) -> String {
pub struct Rating(u8); pub struct Rating(u8);
impl Rating { impl Rating {
const MIN: u8 = 1;
const MAX: u8 = 5; const MAX: u8 = 5;
pub fn new(value: u8) -> Result<Self, DomainError> { pub fn new(value: u8) -> Result<Self, DomainError> {
if value <= Self::MAX { if (Self::MIN..=Self::MAX).contains(&value) {
Ok(Self(value)) Ok(Self(value))
} else { } else {
Err(DomainError::InvalidRating { Err(DomainError::InvalidRating {