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"
jsonwebtoken = "8.3"
tera = "1"
genpdf = "0.2"
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()
}
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(
AuthBearer(token): AuthBearer,
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 events;
mod jwt;
mod leaderboard;
mod models;
mod report;
mod user;
@ -46,10 +47,13 @@ async fn main() {
.route("/event", get(events::get_event).post(events::create_event))
.route("/event/preview", get(events::get_events_preview))
.route("/event/future", get(events::get_all_events))
.route("/event/recent", get(events::get_recent_events))
.route("/report", get(report::get_report))
.route("/attending/confirm", put(attending::confirm_attending))
.route("/attending/mark", post(attending::mark_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 {
db_pool,
jwt_encode,

View File

@ -116,11 +116,10 @@ pub struct Claims {
#[derive(
sqlx::Type, Clone, Hash, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize,
)]
pub struct UserReportEntry {
pub struct LeaderBoardEntry {
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")
mod serde_numeric_date {
use serde::{self, Deserialize, Deserializer, Serializer};

View File

@ -1,6 +1,6 @@
use crate::{
jwt::handle_token,
models::{ReportQuery, Role, UserReportEntry},
models::{LeaderBoardEntry, ReportQuery, Role},
AppState,
};
use axum::{
@ -10,6 +10,12 @@ use axum::{
Json,
};
use axum_auth::AuthBearer;
use genpdf::{
elements::{self, TableLayout},
fonts::{self, FontFamily},
style::{Style, StyledString},
Document,
};
use lazy_static::lazy_static;
use serde_json::json;
use sqlx::{query_as, types::chrono::Local};
@ -40,11 +46,11 @@ pub async fn get_report(
};
let result = query_as!(
UserReportEntry,
LeaderBoardEntry,
r#"
SELECT
u.username,
COALESCE(SUM(e.points), 0) AS "points!: i32"
COALESCE(SUM(e.points), 0) AS points
FROM
users u
LEFT JOIN event_attendees ea
@ -53,7 +59,8 @@ pub async fn get_report(
ON ea.event_id = e.id
WHERE
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.is_none(),
@ -67,13 +74,62 @@ pub async fn get_report(
return (
StatusCode::BAD_REQUEST,
Json(json!({
"error": format!("Unknown error creating event: {}", err)
"error": format!("Unknown error creating event: {:?}", err)
})),
)
.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.
let mut context = Context::new();
context.insert("grade", &report_query.grade);
@ -97,3 +153,70 @@ pub async fn get_report(
println!("{}", report);
(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()
}
};
}