Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions frontend/styles/events/attendees.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
@apply flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between;
}

.info {
@apply flex flex-col;
time, .stats {
@apply text-lsd-gray text-xs;
}
}

.table-wrapper {
@apply overflow-x-scroll;
}
Expand Down
3 changes: 3 additions & 0 deletions frontend/styles/events/list.css
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
time {
@apply text-lsd-gray mb-0.5 text-xs;
}
.stats {
@apply text-lsd-gray mb-1 text-xs;
}

.actions {
@apply flex flex-wrap gap-2 text-xs;
Expand Down
10 changes: 8 additions & 2 deletions frontend/templates/events/attendees.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@
{% block content %}
<section id="events/attendees" class="ext/layout">
<header>
<h1>{{ event.title }}</h1>
<div class="info">
<h1>{{ event.title }}</h1>
<time datetime="{{ event.start.date() }}">
{{ event.start | format_datetime("%a %b %d, %Y") }}
</time>
<span class="stats">{{ rsvp_count }} RSVPs • ${{ total_contributions }} in contributions</span>
</div>
<div class="actions">
<a
href="/events/{{ event.slug }}/attendees/add"
Expand Down Expand Up @@ -34,7 +40,7 @@ <h1>{{ event.title }}</h1>
data-user-id="{{ rsvp.user_id }}"
data-is-manual="{{ rsvp.is_manual }}"
>
<td class="name">{{ rsvp.last_name }}, {{ rsvp.first_name }}</td>
<td class="name">{{ rsvp.first_name }} {{ rsvp.last_name }}</td>
<td class="email">{{ rsvp.email }}</td>
<td class="guest-of">{{ rsvp.guest_of | unwrap_or_empty }}</td>
<td class="spot">
Expand Down
8 changes: 8 additions & 0 deletions frontend/templates/events/edit.html
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ <h1>Edit Event</h1>
id="slug"
name="slug"
value="{{ event.slug }}"
pattern="[a-zA-Z0-9\-]+"
title="Only letters, numbers, and dashes allowed"
required
/>
</div>
Expand Down Expand Up @@ -349,6 +351,12 @@ <h2>Available Spots</h2>
return;
}

const slug = $("slug").value;
if (!slug || !/^[a-zA-Z0-9\-]+$/.test(slug)) {
alert("Slug can only contain letters, numbers, and dashes.");
return;
}

// Retrieve the value of an input, with adjustments for HTML input type weirdness
let value = (name, root = ui.form) => {
const el = root.querySelector(`[name="${name}"]`);
Expand Down
1 change: 1 addition & 0 deletions frontend/templates/events/list.html
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ <h2 class="title">
<time datetime="{{ event.start.date() }}">
{{ event.start | format_datetime("%a %b %d, %Y") }}
</time>
<span class="stats">{{ event.rsvp_count }} RSVPs • ${{ event.total_contributions }} in contributions</span>
<div class="actions">
<a class="ext/button :icon" href="/events/{{ event.slug }}/edit">
<svg
Expand Down
2 changes: 1 addition & 1 deletion frontend/templates/events/rsvp_contribution.html
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@
method="POST"
>
<button class="next" type="submit">
<span>RSVP now</span>
<span>Confirm RSVP</span>
<div class="right">
<span>&rarr;</span>
</div>
Expand Down
29 changes: 26 additions & 3 deletions src/app/events.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use image::DynamicImage;

use crate::db::event::{Event, EventLimits, EventWithRsvpCount, UpdateEvent};
use crate::db::event::{Event, EventLimits, EventWithStats, UpdateEvent};
use crate::db::event_flyer::*;
use crate::db::rsvp_session::*;
use crate::db::spot::*;
Expand Down Expand Up @@ -73,7 +73,7 @@ mod read {
#[template(path = "events/list.html")]
struct ListHtml {
user: Option<User>,
events: Vec<EventWithRsvpCount>,
events: Vec<EventWithStats>,
}

pub async fn list_page(user: Option<User>, State(state): State<SharedAppState>) -> HtmlResult {
Expand Down Expand Up @@ -241,6 +241,14 @@ mod edit {

let form = form.ok_or_else(invalid)?;

// Validate slug: must be non-empty and only contain alphanumeric characters and dashes
if form.event.slug.is_empty()
|| !form.event.slug.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
{
return Ok((StatusCode::BAD_REQUEST, "Slug can only contain letters, numbers, and dashes.")
.into_response());
}

match form.id {
Some(id) => {
Event::update(&state.db, id, &form.event, &flyer).await?;
Expand Down Expand Up @@ -834,10 +842,14 @@ mod edit {
struct Html {
pub user: Option<User>,
event: Event,
rsvp_count: usize,
total_contributions: i64,
rsvps: Vec<AdminAttendeesRsvp>,
}

Ok(Html { user: Some(user), event, rsvps }.into_response())
let rsvp_count = rsvps.len();
let total_contributions = rsvps.iter().map(|r| r.contribution).sum();
Ok(Html { user: Some(user), event, rsvp_count, total_contributions, rsvps }.into_response())
}

/// Handle delete submission.
Expand Down Expand Up @@ -1057,13 +1069,24 @@ mod rsvp {

match List::has_user_id(&state.db, guest_list_id, user.id).await? {
true => {
tracing::info!(
"Guestlist check passed with event_id={} user_id={} user_email={:?}",
event.id,
user.id,
user.email
);

// Check for conflicts
let other_users =
Rsvp::list_reserved_users_for_event(&state.db, &event, session.as_ref()).await?;
use validate::Conflict;
if let Some(Conflict::Guest { email, status } | Conflict::Primary { email, status }) =
validate::no_conflicts(&other_users, &primary_user, &[])
{
tracing::info!(
"RSVP conflict detected with event_id={} conflict_email={email:?} status={status:?}",
event.id
);
return goto::error_conflict(&email, &status);
}

Expand Down
17 changes: 13 additions & 4 deletions src/db/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,19 +56,20 @@ pub struct UpdateEvent {

/// Event with RSVP count for the admin list page.
#[derive(Clone, Debug, sqlx::FromRow, serde::Serialize)]
pub struct EventWithRsvpCount {
pub struct EventWithStats {
pub id: i64,
pub title: String,
pub slug: String,
pub start: NaiveDateTime,
pub guest_list_id: Option<i64>,
pub rsvp_count: i64,
pub total_contributions: i64,
}

impl Event {
pub async fn list(db: &Db) -> Result<Vec<EventWithRsvpCount>> {
pub async fn list(db: &Db) -> Result<Vec<EventWithStats>> {
let events = sqlx::query_as!(
EventWithRsvpCount,
EventWithStats,
r#"SELECT
e.id, e.title, e.slug, e.start, e.guest_list_id,
COALESCE(
Expand All @@ -78,7 +79,15 @@ impl Event {
WHERE rs.event_id = e.id
AND rs.status IN ('payment_pending', 'payment_confirmed')),
0
) as "rsvp_count!: i64"
) as "rsvp_count!: i64",
COALESCE(
(SELECT SUM(r.contribution)
FROM rsvps r
JOIN rsvp_sessions rs ON rs.id = r.session_id
WHERE rs.event_id = e.id
AND rs.status IN ('payment_pending', 'payment_confirmed')),
0
) as "total_contributions!: i64"
FROM events e"#
)
.fetch_all(db)
Expand Down
2 changes: 1 addition & 1 deletion src/db/rsvp.rs
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ impl Rsvp {
JOIN users cu ON cu.id = mr.creator_user_id
WHERE mr.event_id = ?

ORDER BY 3;
ORDER BY 9;
"#,
event_id,
event_id
Expand Down
30 changes: 30 additions & 0 deletions src/db/rsvp_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ impl RsvpSession {
let token = format!("{:08x}", OsRng.r#gen::<u64>());
let user = user.as_ref();
let user_id = user.map(|u| u.id);
let user_email = user.map(|u| u.email.as_str());
let user_version = user.map(|u| u.version);

let session = sqlx::query_as!(
Expand All @@ -113,10 +114,20 @@ impl RsvpSession {
.fetch_one(db)
.await?;

tracing::info!(
"Created RSVP session with session_id={} event_id={event_id} user_id={user_id:?} user_email={user_email:?}",
session.id
);
Ok(session)
}

pub async fn delete(&self, db: &Db) -> Result<()> {
tracing::info!(
"Deleting RSVP session with session_id={} event_id={} status={:?}",
self.id,
self.event_id,
self.status
);
sqlx::query!("DELETE FROM rsvps WHERE session_id = ?", self.id)
.execute(db)
.await?;
Expand All @@ -127,6 +138,11 @@ impl RsvpSession {
}

pub async fn takeover_for_event(&self, db: &Db, event: &Event, email: &str) -> Result<()> {
tracing::info!(
"RSVP session takeover with session_id={} event_id={} taking_over_email={email:?}",
self.id,
event.id,
);
sqlx::query!(
"DELETE FROM rsvp_sessions
WHERE user_id IN (
Expand All @@ -147,6 +163,13 @@ impl RsvpSession {

/// Update
pub async fn set_user(&mut self, db: &Db, user: &User) -> Result<()> {
tracing::info!(
"Setting user on RSVP session with session_id={} event_id={} user_id={} user_email={:?}",
self.id,
self.event_id,
user.id,
user.email
);
sqlx::query!(
"UPDATE rsvp_sessions
SET user_id = ?,
Expand All @@ -165,6 +188,13 @@ impl RsvpSession {
}

pub async fn set_status(&self, db: &Db, status: &str) -> Result<()> {
tracing::info!(
"RSVP status transition with session_id={} event_id={} user_id={:?} status={:?} -> {status:?}",
self.id,
self.event_id,
self.user_id,
self.status,
);
sqlx::query!(
"UPDATE rsvp_sessions
SET status = ?,
Expand Down
16 changes: 14 additions & 2 deletions src/utils/tracing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ use axum::http::Request;
use tower::ServiceBuilder;
use tower_http::ServiceBuilderExt as _;
use tower_http::request_id::{MakeRequestId, RequestId};
use tower_http::trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer};
use tower_http::trace::{DefaultOnResponse, MakeSpan, TraceLayer};
use tracing::Span;
use uuid::Uuid;

use crate::prelude::*;
Expand All @@ -17,14 +18,25 @@ impl MakeRequestId for MakeRequestUuidV7 {
}
}

#[derive(Clone, Copy)]
pub struct LoggingMakeSpan;
impl<B> MakeSpan<B> for LoggingMakeSpan {
fn make_span(&mut self, request: &Request<B>) -> Span {
let method = request.method();
let path = request.uri().path();
tracing::info!("{method} {path:?}");
tracing::span!(tracing::Level::DEBUG, "request", %method, %path)
}
}

/// Register tracing-related middleware into the router.
pub fn add_middleware(router: AxumRouter) -> AxumRouter {
router.layer(
ServiceBuilder::new()
.set_x_request_id(MakeRequestUuidV7)
.layer(
TraceLayer::new_for_http()
.make_span_with(DefaultMakeSpan::new())
.make_span_with(LoggingMakeSpan)
.on_response(DefaultOnResponse::new()),
)
.propagate_x_request_id(),
Expand Down
Loading