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",
|
||||
"domain",
|
||||
"serde",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -8,6 +8,7 @@ askama = { version = "0.16.0" }
|
||||
|
||||
serde = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
|
||||
domain = { workspace = true }
|
||||
application = { workspace = true }
|
||||
|
||||
@@ -17,6 +17,8 @@ struct DiaryTemplate<'a> {
|
||||
limit: u32,
|
||||
has_more: bool,
|
||||
ctx: &'a HtmlPageContext,
|
||||
total_pages: u32,
|
||||
current_page: u32,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@@ -48,6 +50,8 @@ struct ActivityFeedTemplate<'a> {
|
||||
limit: u32,
|
||||
has_more: bool,
|
||||
ctx: &'a HtmlPageContext,
|
||||
total_pages: u32,
|
||||
current_page: u32,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@@ -67,6 +71,7 @@ struct MonthlyRatingRow<'a> {
|
||||
struct ProfileTemplate<'a> {
|
||||
ctx: &'a HtmlPageContext,
|
||||
profile_display_name: String,
|
||||
profile_user_id: uuid::Uuid,
|
||||
stats: &'a UserStats,
|
||||
view: &'a str,
|
||||
entries: Option<&'a Paginated<DiaryEntry>>,
|
||||
@@ -77,6 +82,8 @@ struct ProfileTemplate<'a> {
|
||||
trends: Option<&'a UserTrends>,
|
||||
monthly_rating_rows: Vec<MonthlyRatingRow<'a>>,
|
||||
heatmap: Vec<HeatmapCell>,
|
||||
total_pages: u32,
|
||||
current_page: u32,
|
||||
}
|
||||
|
||||
struct HeatmapCell {
|
||||
@@ -140,12 +147,21 @@ impl AskamaHtmlRenderer {
|
||||
impl HtmlRenderer for AskamaHtmlRenderer {
|
||||
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 (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 {
|
||||
entries: &data.items,
|
||||
current_offset: data.offset,
|
||||
limit: data.limit,
|
||||
has_more,
|
||||
ctx: &ctx,
|
||||
total_pages,
|
||||
current_page,
|
||||
}
|
||||
.render()
|
||||
.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> {
|
||||
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 {
|
||||
entries: &data.entries.items,
|
||||
current_offset: data.current_offset,
|
||||
limit: data.limit,
|
||||
limit,
|
||||
has_more: data.has_more,
|
||||
ctx: &data.ctx,
|
||||
total_pages,
|
||||
current_page,
|
||||
}
|
||||
.render()
|
||||
.map_err(|e| e.to_string())
|
||||
@@ -211,9 +233,14 @@ impl HtmlRenderer for AskamaHtmlRenderer {
|
||||
rating: r,
|
||||
}).collect())
|
||||
.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 {
|
||||
ctx: &data.ctx,
|
||||
profile_display_name,
|
||||
profile_user_id: data.profile_user_id,
|
||||
stats: &data.stats,
|
||||
view: &data.view,
|
||||
entries: data.entries.as_ref(),
|
||||
@@ -224,6 +251,8 @@ impl HtmlRenderer for AskamaHtmlRenderer {
|
||||
trends: data.trends.as_ref(),
|
||||
monthly_rating_rows,
|
||||
heatmap,
|
||||
total_pages,
|
||||
current_page,
|
||||
}
|
||||
.render()
|
||||
.map_err(|e| e.to_string())
|
||||
|
||||
@@ -30,6 +30,7 @@
|
||||
</div>
|
||||
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
||||
<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>
|
||||
</form>
|
||||
{% endif %}
|
||||
@@ -41,10 +42,17 @@
|
||||
</div>
|
||||
<nav class="pagination">
|
||||
{% if current_offset >= limit %}
|
||||
<a href="/?offset={{ current_offset - limit }}">← Prev</a>
|
||||
<a href="/?offset={{ current_offset - limit }}" class="page-nav">← Prev</a>
|
||||
{% 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 %}
|
||||
<a href="/?offset={{ current_offset + limit }}">Next →</a>
|
||||
<a href="/?offset={{ current_offset + limit }}" class="page-nav">Next →</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
@@ -42,10 +42,17 @@
|
||||
</div>
|
||||
<nav class="pagination">
|
||||
{% if current_offset > 0 %}
|
||||
<a href="/?offset={{ current_offset - limit }}">← Prev</a>
|
||||
<a href="/?offset={{ current_offset - limit }}" class="page-nav">← Prev</a>
|
||||
{% 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 %}
|
||||
<a href="/?offset={{ current_offset + limit }}">Next →</a>
|
||||
<a href="/?offset={{ current_offset + limit }}" class="page-nav">Next →</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endblock %}
|
||||
|
||||
@@ -29,7 +29,7 @@
|
||||
</label>
|
||||
<label>
|
||||
Watched<br>
|
||||
<input type="datetime-local" name="watched_at" required>
|
||||
<input type="date" name="watched_at" required>
|
||||
</label>
|
||||
<label>
|
||||
Comment<br>
|
||||
|
||||
@@ -141,6 +141,7 @@
|
||||
<div class="watched-at">{{ entry.review().watched_at().format("%Y-%m-%d") }}</div>
|
||||
{% if ctx.is_current_user(entry.review().user_id().value()) %}
|
||||
<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>
|
||||
</form>
|
||||
{% endif %}
|
||||
@@ -152,10 +153,17 @@
|
||||
</div>
|
||||
<nav class="pagination">
|
||||
{% 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 %}
|
||||
{% 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 %}
|
||||
<a href="?view={{ view }}&offset={{ current_offset + limit }}">Next →</a>
|
||||
<a href="?view={{ view }}&offset={{ current_offset + limit }}" class="page-nav">Next →</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
{% endif %}
|
||||
|
||||
@@ -59,6 +59,12 @@ pub struct ErrorQuery {
|
||||
pub error: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Default)]
|
||||
pub struct DeleteRedirectForm {
|
||||
#[serde(default)]
|
||||
pub redirect_after: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct LogReviewRequest {
|
||||
pub external_metadata_id: Option<String>,
|
||||
@@ -150,10 +156,14 @@ impl TryFrom<LogReviewForm> for LogReviewData {
|
||||
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")
|
||||
.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 {
|
||||
field: "watched_at",
|
||||
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
|
||||
),
|
||||
})?;
|
||||
@@ -371,4 +381,11 @@ mod tests {
|
||||
let req: LoginRequest = serde_json::from_str(json).unwrap();
|
||||
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>,
|
||||
RequiredCookieUser(user_id): RequiredCookieUser,
|
||||
Path(review_id): Path<Uuid>,
|
||||
Form(form): Form<crate::dtos::DeleteRedirectForm>,
|
||||
) -> impl IntoResponse {
|
||||
let cmd = DeleteReviewCommand {
|
||||
review_id,
|
||||
requesting_user_id: user_id.value(),
|
||||
};
|
||||
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::Unauthorized(_)) => StatusCode::FORBIDDEN.into_response(),
|
||||
Err(e) => {
|
||||
|
||||
Reference in New Issue
Block a user