started pdf report impl
This commit is contained in:
parent
44b9068c6f
commit
9b630e8f7e
@ -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
BIN
fonts/Lato-Black.ttf
Normal file
Binary file not shown.
BIN
fonts/Lato-BlackItalic.ttf
Normal file
BIN
fonts/Lato-BlackItalic.ttf
Normal file
Binary file not shown.
BIN
fonts/Lato-Bold.ttf
Normal file
BIN
fonts/Lato-Bold.ttf
Normal file
Binary file not shown.
BIN
fonts/Lato-BoldItalic.ttf
Normal file
BIN
fonts/Lato-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
fonts/Lato-Italic.ttf
Normal file
BIN
fonts/Lato-Italic.ttf
Normal file
Binary file not shown.
BIN
fonts/Lato-Light.ttf
Normal file
BIN
fonts/Lato-Light.ttf
Normal file
Binary file not shown.
BIN
fonts/Lato-LightItalic.ttf
Normal file
BIN
fonts/Lato-LightItalic.ttf
Normal file
Binary file not shown.
BIN
fonts/Lato-Regular.ttf
Normal file
BIN
fonts/Lato-Regular.ttf
Normal file
Binary file not shown.
BIN
fonts/Lato-Thin.ttf
Normal file
BIN
fonts/Lato-Thin.ttf
Normal file
Binary file not shown.
BIN
fonts/Lato-ThinItalic.ttf
Normal file
BIN
fonts/Lato-ThinItalic.ttf
Normal file
Binary file not shown.
@ -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
88
src/leaderboard.rs
Normal 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()
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
|||||||
@ -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};
|
||||||
|
|||||||
133
src/report.rs
133
src/report.rs
@ -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(¤t_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()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user