Compare commits
6 Commits
31dba4ca95
...
9cda5b4681
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cda5b4681 | |||
| 74fb893751 | |||
| 32b114cecd | |||
| bd571b3b51 | |||
| f226071cbd | |||
| aaa9cb0a1e |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -3951,6 +3951,7 @@ dependencies = [
|
|||||||
"chrono",
|
"chrono",
|
||||||
"domain",
|
"domain",
|
||||||
"serde",
|
"serde",
|
||||||
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ askama = { version = "0.16.0" }
|
|||||||
|
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
chrono = { workspace = true }
|
chrono = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
|
|
||||||
domain = { workspace = true }
|
domain = { workspace = true }
|
||||||
application = { workspace = true }
|
application = { workspace = true }
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ struct DiaryTemplate<'a> {
|
|||||||
limit: u32,
|
limit: u32,
|
||||||
has_more: bool,
|
has_more: bool,
|
||||||
ctx: &'a HtmlPageContext,
|
ctx: &'a HtmlPageContext,
|
||||||
|
total_pages: u32,
|
||||||
|
current_page: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
@@ -48,6 +50,8 @@ struct ActivityFeedTemplate<'a> {
|
|||||||
limit: u32,
|
limit: u32,
|
||||||
has_more: bool,
|
has_more: bool,
|
||||||
ctx: &'a HtmlPageContext,
|
ctx: &'a HtmlPageContext,
|
||||||
|
total_pages: u32,
|
||||||
|
current_page: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
@@ -67,6 +71,7 @@ struct MonthlyRatingRow<'a> {
|
|||||||
struct ProfileTemplate<'a> {
|
struct ProfileTemplate<'a> {
|
||||||
ctx: &'a HtmlPageContext,
|
ctx: &'a HtmlPageContext,
|
||||||
profile_display_name: String,
|
profile_display_name: String,
|
||||||
|
profile_user_id: uuid::Uuid,
|
||||||
stats: &'a UserStats,
|
stats: &'a UserStats,
|
||||||
view: &'a str,
|
view: &'a str,
|
||||||
entries: Option<&'a Paginated<DiaryEntry>>,
|
entries: Option<&'a Paginated<DiaryEntry>>,
|
||||||
@@ -77,6 +82,8 @@ struct ProfileTemplate<'a> {
|
|||||||
trends: Option<&'a UserTrends>,
|
trends: Option<&'a UserTrends>,
|
||||||
monthly_rating_rows: Vec<MonthlyRatingRow<'a>>,
|
monthly_rating_rows: Vec<MonthlyRatingRow<'a>>,
|
||||||
heatmap: Vec<HeatmapCell>,
|
heatmap: Vec<HeatmapCell>,
|
||||||
|
total_pages: u32,
|
||||||
|
current_page: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct HeatmapCell {
|
struct HeatmapCell {
|
||||||
@@ -140,12 +147,21 @@ impl AskamaHtmlRenderer {
|
|||||||
impl HtmlRenderer for AskamaHtmlRenderer {
|
impl HtmlRenderer for AskamaHtmlRenderer {
|
||||||
fn render_diary_page(&self, data: &Paginated<DiaryEntry>, ctx: HtmlPageContext) -> Result<String, String> {
|
fn render_diary_page(&self, data: &Paginated<DiaryEntry>, ctx: HtmlPageContext) -> Result<String, String> {
|
||||||
let has_more = (data.offset + data.limit) < data.total_count as u32;
|
let has_more = (data.offset + data.limit) < data.total_count as u32;
|
||||||
|
let (total_pages, current_page) = if data.limit > 0 {
|
||||||
|
let total_pages = ((data.total_count + data.limit as u64 - 1) / data.limit as u64) as u32;
|
||||||
|
let current_page = data.offset / data.limit;
|
||||||
|
(total_pages, current_page)
|
||||||
|
} else {
|
||||||
|
(0, 0)
|
||||||
|
};
|
||||||
DiaryTemplate {
|
DiaryTemplate {
|
||||||
entries: &data.items,
|
entries: &data.items,
|
||||||
current_offset: data.offset,
|
current_offset: data.offset,
|
||||||
limit: data.limit,
|
limit: data.limit,
|
||||||
has_more,
|
has_more,
|
||||||
ctx: &ctx,
|
ctx: &ctx,
|
||||||
|
total_pages,
|
||||||
|
current_page,
|
||||||
}
|
}
|
||||||
.render()
|
.render()
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
@@ -179,12 +195,18 @@ impl HtmlRenderer for AskamaHtmlRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn render_activity_feed_page(&self, data: ActivityFeedPageData) -> Result<String, String> {
|
fn render_activity_feed_page(&self, data: ActivityFeedPageData) -> Result<String, String> {
|
||||||
|
let total_count = data.entries.total_count;
|
||||||
|
let limit = data.limit;
|
||||||
|
let total_pages = ((total_count + limit as u64 - 1) / limit as u64) as u32;
|
||||||
|
let current_page = if limit > 0 { data.current_offset / limit } else { 0 };
|
||||||
ActivityFeedTemplate {
|
ActivityFeedTemplate {
|
||||||
entries: &data.entries.items,
|
entries: &data.entries.items,
|
||||||
current_offset: data.current_offset,
|
current_offset: data.current_offset,
|
||||||
limit: data.limit,
|
limit,
|
||||||
has_more: data.has_more,
|
has_more: data.has_more,
|
||||||
ctx: &data.ctx,
|
ctx: &data.ctx,
|
||||||
|
total_pages,
|
||||||
|
current_page,
|
||||||
}
|
}
|
||||||
.render()
|
.render()
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
@@ -211,9 +233,14 @@ impl HtmlRenderer for AskamaHtmlRenderer {
|
|||||||
rating: r,
|
rating: r,
|
||||||
}).collect())
|
}).collect())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
let total_pages = data.entries.as_ref()
|
||||||
|
.map(|e| ((e.total_count + e.limit as u64 - 1) / e.limit as u64) as u32)
|
||||||
|
.unwrap_or(0);
|
||||||
|
let current_page = if data.limit > 0 { data.current_offset / data.limit } else { 0 };
|
||||||
ProfileTemplate {
|
ProfileTemplate {
|
||||||
ctx: &data.ctx,
|
ctx: &data.ctx,
|
||||||
profile_display_name,
|
profile_display_name,
|
||||||
|
profile_user_id: data.profile_user_id,
|
||||||
stats: &data.stats,
|
stats: &data.stats,
|
||||||
view: &data.view,
|
view: &data.view,
|
||||||
entries: data.entries.as_ref(),
|
entries: data.entries.as_ref(),
|
||||||
@@ -224,6 +251,8 @@ impl HtmlRenderer for AskamaHtmlRenderer {
|
|||||||
trends: data.trends.as_ref(),
|
trends: data.trends.as_ref(),
|
||||||
monthly_rating_rows,
|
monthly_rating_rows,
|
||||||
heatmap,
|
heatmap,
|
||||||
|
total_pages,
|
||||||
|
current_page,
|
||||||
}
|
}
|
||||||
.render()
|
.render()
|
||||||
.map_err(|e| e.to_string())
|
.map_err(|e| e.to_string())
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
||||||
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
|
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
|
||||||
|
<input type="hidden" name="redirect_after" value="/?offset={{ current_offset }}">
|
||||||
<button type="submit">Delete</button>
|
<button type="submit">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -41,10 +42,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<nav class="pagination">
|
<nav class="pagination">
|
||||||
{% if current_offset >= limit %}
|
{% if current_offset >= limit %}
|
||||||
<a href="/?offset={{ current_offset - limit }}">← Prev</a>
|
<a href="/?offset={{ current_offset - limit }}" class="page-nav">← Prev</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% for p in (0..total_pages) %}
|
||||||
|
{% if p == current_page %}
|
||||||
|
<span class="page-num current">{{ p + 1 }}</span>
|
||||||
|
{% else %}
|
||||||
|
<a href="/?offset={{ p * limit }}" class="page-num">{{ p + 1 }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
{% if has_more %}
|
{% if has_more %}
|
||||||
<a href="/?offset={{ current_offset + limit }}">Next →</a>
|
<a href="/?offset={{ current_offset + limit }}" class="page-nav">Next →</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -42,10 +42,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<nav class="pagination">
|
<nav class="pagination">
|
||||||
{% if current_offset > 0 %}
|
{% if current_offset > 0 %}
|
||||||
<a href="/?offset={{ current_offset - limit }}">← Prev</a>
|
<a href="/?offset={{ current_offset - limit }}" class="page-nav">← Prev</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% for p in (0..total_pages) %}
|
||||||
|
{% if p == current_page %}
|
||||||
|
<span class="page-num current">{{ p + 1 }}</span>
|
||||||
|
{% else %}
|
||||||
|
<a href="/?offset={{ p * limit }}" class="page-num">{{ p + 1 }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
{% if has_more %}
|
{% if has_more %}
|
||||||
<a href="/?offset={{ current_offset + limit }}">Next →</a>
|
<a href="/?offset={{ current_offset + limit }}" class="page-nav">Next →</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Watched<br>
|
Watched<br>
|
||||||
<input type="datetime-local" name="watched_at" required>
|
<input type="date" name="watched_at" required>
|
||||||
</label>
|
</label>
|
||||||
<label>
|
<label>
|
||||||
Comment<br>
|
Comment<br>
|
||||||
|
|||||||
@@ -141,6 +141,7 @@
|
|||||||
<div class="watched-at">{{ entry.review().watched_at().format("%Y-%m-%d") }}</div>
|
<div class="watched-at">{{ entry.review().watched_at().format("%Y-%m-%d") }}</div>
|
||||||
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
||||||
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
|
<form method="post" action="/reviews/{{ entry.review().id().value() }}/delete" class="delete-form">
|
||||||
|
<input type="hidden" name="redirect_after" value="/users/{{ profile_user_id }}?view={{ view }}&offset={{ current_offset }}">
|
||||||
<button type="submit">Delete</button>
|
<button type="submit">Delete</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -152,10 +153,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<nav class="pagination">
|
<nav class="pagination">
|
||||||
{% if current_offset >= limit %}
|
{% if current_offset >= limit %}
|
||||||
<a href="?view={{ view }}&offset={{ current_offset - limit }}">← Prev</a>
|
<a href="?view={{ view }}&offset={{ current_offset - limit }}" class="page-nav">← Prev</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% for p in (0..total_pages) %}
|
||||||
|
{% if p == current_page %}
|
||||||
|
<span class="page-num current">{{ p + 1 }}</span>
|
||||||
|
{% else %}
|
||||||
|
<a href="?view={{ view }}&offset={{ p * limit }}" class="page-num">{{ p + 1 }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
{% if has_more %}
|
{% if has_more %}
|
||||||
<a href="?view={{ view }}&offset={{ current_offset + limit }}">Next →</a>
|
<a href="?view={{ view }}&offset={{ current_offset + limit }}" class="page-nav">Next →</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</nav>
|
</nav>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -59,6 +59,12 @@ pub struct ErrorQuery {
|
|||||||
pub error: Option<String>,
|
pub error: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Default)]
|
||||||
|
pub struct DeleteRedirectForm {
|
||||||
|
#[serde(default)]
|
||||||
|
pub redirect_after: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct LogReviewRequest {
|
pub struct LogReviewRequest {
|
||||||
pub external_metadata_id: Option<String>,
|
pub external_metadata_id: Option<String>,
|
||||||
@@ -150,10 +156,14 @@ impl TryFrom<LogReviewForm> for LogReviewData {
|
|||||||
fn try_from(form: LogReviewForm) -> Result<Self, Self::Error> {
|
fn try_from(form: LogReviewForm) -> Result<Self, Self::Error> {
|
||||||
let watched_at = NaiveDateTime::parse_from_str(&form.watched_at, "%Y-%m-%dT%H:%M:%S")
|
let watched_at = NaiveDateTime::parse_from_str(&form.watched_at, "%Y-%m-%dT%H:%M:%S")
|
||||||
.or_else(|_| NaiveDateTime::parse_from_str(&form.watched_at, "%Y-%m-%dT%H:%M"))
|
.or_else(|_| NaiveDateTime::parse_from_str(&form.watched_at, "%Y-%m-%dT%H:%M"))
|
||||||
|
.or_else(|_| {
|
||||||
|
chrono::NaiveDate::parse_from_str(&form.watched_at, "%Y-%m-%d")
|
||||||
|
.map(|d| d.and_hms_opt(0, 0, 0).expect("midnight always valid"))
|
||||||
|
})
|
||||||
.map_err(|_| ParseReviewError {
|
.map_err(|_| ParseReviewError {
|
||||||
field: "watched_at",
|
field: "watched_at",
|
||||||
message: format!(
|
message: format!(
|
||||||
"invalid date '{}'; expected YYYY-MM-DDTHH:MM[:SS]",
|
"invalid date '{}'; expected YYYY-MM-DD or YYYY-MM-DDTHH:MM[:SS]",
|
||||||
form.watched_at
|
form.watched_at
|
||||||
),
|
),
|
||||||
})?;
|
})?;
|
||||||
@@ -371,4 +381,11 @@ mod tests {
|
|||||||
let req: LoginRequest = serde_json::from_str(json).unwrap();
|
let req: LoginRequest = serde_json::from_str(json).unwrap();
|
||||||
assert_eq!(req.email, "a@b.com");
|
assert_eq!(req.email, "a@b.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn form_accepts_date_only() {
|
||||||
|
let data = LogReviewData::try_from(make_form("2024-03-15")).unwrap();
|
||||||
|
assert_eq!(data.watched_at.format("%H:%M:%S").to_string(), "00:00:00");
|
||||||
|
assert_eq!(data.watched_at.format("%Y-%m-%d").to_string(), "2024-03-15");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,13 +219,20 @@ pub mod html {
|
|||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||||
Path(review_id): Path<Uuid>,
|
Path(review_id): Path<Uuid>,
|
||||||
|
Form(form): Form<crate::dtos::DeleteRedirectForm>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let cmd = DeleteReviewCommand {
|
let cmd = DeleteReviewCommand {
|
||||||
review_id,
|
review_id,
|
||||||
requesting_user_id: user_id.value(),
|
requesting_user_id: user_id.value(),
|
||||||
};
|
};
|
||||||
match delete_review::execute(&state.app_ctx, cmd).await {
|
match delete_review::execute(&state.app_ctx, cmd).await {
|
||||||
Ok(()) => Redirect::to("/").into_response(),
|
Ok(()) => {
|
||||||
|
let redirect_url = form
|
||||||
|
.redirect_after
|
||||||
|
.filter(|url| (url.starts_with('/') && !url.starts_with("//")) || url.starts_with('?'))
|
||||||
|
.unwrap_or_else(|| "/".to_string());
|
||||||
|
Redirect::to(&redirect_url).into_response()
|
||||||
|
}
|
||||||
Err(DomainError::NotFound(_)) => StatusCode::NOT_FOUND.into_response(),
|
Err(DomainError::NotFound(_)) => StatusCode::NOT_FOUND.into_response(),
|
||||||
Err(DomainError::Unauthorized(_)) => StatusCode::FORBIDDEN.into_response(),
|
Err(DomainError::Unauthorized(_)) => StatusCode::FORBIDDEN.into_response(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user