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
+1
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"
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+47
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
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()
}
+4
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,
+2 -3
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};
+128 -5
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()
}
};
}