started pdf report impl
This commit is contained in:
@@ -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"
|
||||
|
||||
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.
@@ -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>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+2
-3
@@ -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};
|
||||
|
||||
+128
-5
@@ -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()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user