started pdf report impl

This commit is contained in:
Mitchell Marino 2023-04-15 23:52:02 -05:00
parent 44b9068c6f
commit 9b630e8f7e
16 changed files with 270 additions and 8 deletions

View File

@ -17,4 +17,5 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
sha256 = "1.1" sha256 = "1.1"
jsonwebtoken = "8.3" jsonwebtoken = "8.3"
tera = "1" tera = "1"
genpdf = "0.2"
lazy_static = "1.4" lazy_static = "1.4"

BIN
fonts/Lato-Black.ttf Normal file

Binary file not shown.

BIN
fonts/Lato-BlackItalic.ttf Normal file

Binary file not shown.

BIN
fonts/Lato-Bold.ttf Normal file

Binary file not shown.

BIN
fonts/Lato-BoldItalic.ttf Normal file

Binary file not shown.

BIN
fonts/Lato-Italic.ttf Normal file

Binary file not shown.

BIN
fonts/Lato-Light.ttf Normal file

Binary file not shown.

BIN
fonts/Lato-LightItalic.ttf Normal file

Binary file not shown.

BIN
fonts/Lato-Regular.ttf Normal file

Binary file not shown.

BIN
fonts/Lato-Thin.ttf Normal file

Binary file not shown.

BIN
fonts/Lato-ThinItalic.ttf Normal file

Binary file not shown.

View File

@ -110,6 +110,53 @@ LIMIT
.into_response() .into_response()
} }
pub async fn get_recent_events(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
) -> impl IntoResponse {
let token_data = match handle_token(token, &app_state, Role::Student) {
Ok(token_data) => token_data,
Err(err) => return err,
};
let result = query_as!(
Event,
r#"
SELECT
e.id,
e.title,
e.description,
e.time_start,
e.time_end,
e.event_type AS "event_type!: EventType",
e.points,
e.place,
e.price,
e.created_by
FROM events e
INNER JOIN event_attendees ea
ON ea.event_id = e.id
WHERE
ea.user_id = $1
;
"#,
token_data.id
)
.fetch_all(&app_state.db_pool)
.await;
match result {
Ok(events) => (StatusCode::OK, Json(json!(events))),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Unknown error getting events: {:?}", err)
})),
),
}
.into_response()
}
pub async fn get_event( pub async fn get_event(
AuthBearer(token): AuthBearer, AuthBearer(token): AuthBearer,
State(app_state): State<AppState>, State(app_state): State<AppState>,

88
src/leaderboard.rs Normal file
View File

@ -0,0 +1,88 @@
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
use axum_auth::AuthBearer;
use serde_json::json;
use sqlx::{query, query_as};
use crate::{
jwt::handle_token,
models::{LeaderBoardEntry, Role},
AppState,
};
pub async fn get_points(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
) -> impl IntoResponse {
let token_data = match handle_token(token, &app_state, Role::Student) {
Ok(token_data) => token_data,
Err(err) => return err,
};
let result = query!(
r#"
SELECT
COALESCE(SUM(e.points), 0) AS points
FROM events e
INNER JOIN event_attendees ea
ON ea.event_id = e.id
WHERE
ea.user_id = $1 AND
ea.confirmed = true
;
"#,
token_data.id
)
.fetch_one(&app_state.db_pool)
.await;
match result {
Ok(record) => (StatusCode::OK, Json(json!({ "data": record.points }))),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Unknown error getting events: {:?}", err)
})),
),
}
.into_response()
}
pub async fn list_points(
AuthBearer(token): AuthBearer,
State(app_state): State<AppState>,
) -> impl IntoResponse {
if let Err(err) = handle_token(token, &app_state, Role::Student) {
return err;
};
let result = query_as!(
LeaderBoardEntry,
r#"
SELECT
u.username,
COALESCE(SUM(e.points), 0) AS points
FROM
users u
LEFT JOIN event_attendees ea
ON u.id = ea.user_id AND ea.confirmed = true
LEFT JOIN events e
ON ea.event_id = e.id
GROUP BY u.id
ORDER BY points
;
"#,
)
.fetch_all(&app_state.db_pool)
.await;
match result {
Ok(point_totals) => (StatusCode::OK, Json(json!({ "data": point_totals }))),
Err(err) => (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Unknown error getting events: {:?}", err)
})),
),
}
.into_response()
}

View File

@ -1,6 +1,7 @@
mod attending; mod attending;
mod events; mod events;
mod jwt; mod jwt;
mod leaderboard;
mod models; mod models;
mod report; mod report;
mod user; mod user;
@ -46,10 +47,13 @@ async fn main() {
.route("/event", get(events::get_event).post(events::create_event)) .route("/event", get(events::get_event).post(events::create_event))
.route("/event/preview", get(events::get_events_preview)) .route("/event/preview", get(events::get_events_preview))
.route("/event/future", get(events::get_all_events)) .route("/event/future", get(events::get_all_events))
.route("/event/recent", get(events::get_recent_events))
.route("/report", get(report::get_report)) .route("/report", get(report::get_report))
.route("/attending/confirm", put(attending::confirm_attending)) .route("/attending/confirm", put(attending::confirm_attending))
.route("/attending/mark", post(attending::mark_attending)) .route("/attending/mark", post(attending::mark_attending))
.route("/attending/unmark", delete(attending::unmark_attending)) .route("/attending/unmark", delete(attending::unmark_attending))
.route("/leaderboard/my_points", get(leaderboard::get_points))
.route("/leaderboard/list_points", get(leaderboard::list_points))
.with_state(AppState { .with_state(AppState {
db_pool, db_pool,
jwt_encode, jwt_encode,

View File

@ -116,11 +116,10 @@ pub struct Claims {
#[derive( #[derive(
sqlx::Type, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, sqlx::Type, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
)] )]
pub struct UserReportEntry { pub struct LeaderBoardEntry {
pub username: String, pub username: String,
pub points: i32, pub points: Option<i64>,
} }
/// Module for (de)serializing [OffsetDateTime] to conform with the JWT spec (RFC 7519 section 2, "Numeric Date") /// Module for (de)serializing [OffsetDateTime] to conform with the JWT spec (RFC 7519 section 2, "Numeric Date")
mod serde_numeric_date { mod serde_numeric_date {
use serde::{self, Deserialize, Deserializer, Serializer}; use serde::{self, Deserialize, Deserializer, Serializer};

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
jwt::handle_token, jwt::handle_token,
models::{ReportQuery, Role, UserReportEntry}, models::{LeaderBoardEntry, ReportQuery, Role},
AppState, AppState,
}; };
use axum::{ use axum::{
@ -10,6 +10,12 @@ use axum::{
Json, Json,
}; };
use axum_auth::AuthBearer; use axum_auth::AuthBearer;
use genpdf::{
elements::{self, TableLayout},
fonts::{self, FontFamily},
style::{Style, StyledString},
Document,
};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use serde_json::json; use serde_json::json;
use sqlx::{query_as, types::chrono::Local}; use sqlx::{query_as, types::chrono::Local};
@ -40,11 +46,11 @@ pub async fn get_report(
}; };
let result = query_as!( let result = query_as!(
UserReportEntry, LeaderBoardEntry,
r#" r#"
SELECT SELECT
u.username, u.username,
COALESCE(SUM(e.points), 0) AS "points!: i32" COALESCE(SUM(e.points), 0) AS points
FROM FROM
users u users u
LEFT JOIN event_attendees ea LEFT JOIN event_attendees ea
@ -53,7 +59,8 @@ pub async fn get_report(
ON ea.event_id = e.id ON ea.event_id = e.id
WHERE WHERE
u.grade = $1 OR $2 u.grade = $1 OR $2
GROUP BY u.id; GROUP BY u.id
ORDER BY points
"#, "#,
report_query.grade.unwrap_or(0), report_query.grade.unwrap_or(0),
report_query.grade.is_none(), report_query.grade.is_none(),
@ -67,13 +74,62 @@ pub async fn get_report(
return ( return (
StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST,
Json(json!({ Json(json!({
"error": format!("Unknown error creating event: {}", err) "error": format!("Unknown error creating event: {:?}", err)
})), })),
) )
.into_response() .into_response()
} }
}; };
// Create a new PDF document
let font_family = match fonts::from_files("./fonts", "Lato", None) {
Ok(ff) => ff,
Err(err) => {
return (
StatusCode::INTERNAL_SERVER_ERROR,
Json(json!({
"error": format!("Unknown error rendering pdf: {:?}", err)
})),
)
.into_response()
}
};
let mut pdf = Document::new(font_family);
// Add the current date and the report title to the page
let current_date = Local::now().format("%B %e, %Y").to_string();
pdf.write_text(&current_date, &title_font, 16.0, 30.0, None)?;
let title_text = match report_query.grade {
Some(grade) => format!("Student Point Report Grade {}", grade),
None => "Student Point Report".to_owned(),
};
pdf.set_title(title_text);
let heading_style = Style::new().with_font_size(24).bold();
let heading = elements::Text::new(StyledString::new(title_text, heading_style));
pdf.push(heading);
// Define the table headers
let headers = vec!["Name", "Points"];
// Define the table data
let data = users
.iter()
.map(|(name, points)| vec![name.to_string(), points.to_string()])
.collect();
// Define the table builder and set some options
let table_builder = PdfTableBuilder::new(&page, 10.0, 80.0, 170.0, headers.len() as u32)
.set_font(&header_font, 12.0)
.set_text_alignment(genpdf::Alignment::Center)
.set_row_height(20.0)
.set_header_bg_color((200, 200, 200))
.set_header_text_color((0, 0, 0))
.set_alternate_row_bg_color((240, 240, 240));
// Build the table and add it to the page
let table = table_builder.build_table(headers, data, &body_font)?;
page.add_table(table);
// render report to html. // render report to html.
let mut context = Context::new(); let mut context = Context::new();
context.insert("grade", &report_query.grade); context.insert("grade", &report_query.grade);
@ -97,3 +153,70 @@ pub async fn get_report(
println!("{}", report); println!("{}", report);
(StatusCode::OK, "").into_response() (StatusCode::OK, "").into_response()
} }
fn render_pdf(
grade: Option<i32>,
data: Vec<LeaderBoardEntry>,
) -> Result<String, genpdf::error::Error> {
let font_family = fonts::from_files("./fonts", "Lato", None)?;
let mut pdf = Document::new(font_family);
// Add the current date and the report title to the page
let current_date = Local::now().format("%Y-%m-%d").to_string();
let title_text = match grade {
Some(grade) => format!("Student Point Report Grade {}", grade),
None => "Student Point Report".to_owned(),
};
pdf.set_title(title_text);
let heading_style = Style::new().with_font_size(24).bold();
let heading = elements::Text::new(StyledString::new(title_text, heading_style));
pdf.push(heading);
let date_text = elements::Text::new(current_date);
pdf.push(date_text);
// Define the table headers
let headers = vec!["Name", "Points"];
// Define the table builder and set some options
let mut table = TableLayout::new(vec![2, 1])
.set_cell_decorator(elements::FrameCellDecorator::new(true, true, true));
for row_data in data {
table
.row()
.element(elements::Paragraph::new(row_data.username))
.element(elements::Paragraph::new(
row_data.points.unwrap().to_string(),
))
.push()?;
}
let table_builder = TableLayout::new(&page, 10.0, 80.0, 170.0, headers.len() as u32)
.set_font(&header_font, 12.0)
.set_text_alignment(genpdf::Alignment::Center)
.set_row_height(20.0)
.set_header_bg_color((200, 200, 200))
.set_header_text_color((0, 0, 0))
.set_alternate_row_bg_color((240, 240, 240));
// Build the table and add it to the page
let table = table_builder.build_table(headers, data, &body_font)?;
page.add_table(table);
// render report to html.
let mut context = Context::new();
context.insert("grade", &report_query.grade);
context.insert("date", &Local::now().format("%Y-%m-%d").to_string());
context.insert("users", &records);
let report = match TEMPLATES.render("report.html", &context) {
Ok(report) => report,
Err(err) => {
return (
StatusCode::BAD_REQUEST,
Json(json!({
"error": format!("Unknown error creating event: {:?}", err)
})),
)
.into_response()
}
};
}