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"
|
||||
jsonwebtoken = "8.3"
|
||||
tera = "1"
|
||||
genpdf = "0.2"
|
||||
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()
|
||||
}
|
||||
|
||||
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
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 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,
|
||||
|
||||
@ -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};
|
||||
|
||||
133
src/report.rs
133
src/report.rs
@ -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(¤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.
|
||||
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()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user