mirror of
https://github.com/Xevion/banner.git
synced 2026-01-30 22:23:32 -06:00
137 lines
4.3 KiB
Rust
137 lines
4.3 KiB
Rust
//! Web API endpoints for calendar export (ICS download + Google Calendar redirect).
|
|
|
|
use axum::{
|
|
extract::{Path, State},
|
|
http::{StatusCode, header},
|
|
response::{IntoResponse, Redirect, Response},
|
|
};
|
|
|
|
use crate::calendar::{CalendarCourse, generate_gcal_url, generate_ics};
|
|
use crate::data::models::DbMeetingTime;
|
|
use crate::state::AppState;
|
|
|
|
/// Fetch course + meeting times, build a `CalendarCourse`.
|
|
async fn load_calendar_course(
|
|
state: &AppState,
|
|
term: &str,
|
|
crn: &str,
|
|
) -> Result<(CalendarCourse, Vec<DbMeetingTime>), (StatusCode, String)> {
|
|
let course = crate::data::courses::get_course_by_crn(&state.db_pool, crn, term)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!(error = %e, "Calendar: course lookup failed");
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Lookup failed".to_string(),
|
|
)
|
|
})?
|
|
.ok_or_else(|| (StatusCode::NOT_FOUND, "Course not found".to_string()))?;
|
|
|
|
let instructors = crate::data::courses::get_course_instructors(&state.db_pool, course.id)
|
|
.await
|
|
.unwrap_or_default();
|
|
|
|
let primary_instructor = instructors
|
|
.iter()
|
|
.find(|i| i.is_primary)
|
|
.or(instructors.first())
|
|
.map(|i| i.display_name.clone());
|
|
|
|
let meeting_times: Vec<DbMeetingTime> =
|
|
serde_json::from_value(course.meeting_times.clone()).unwrap_or_default();
|
|
|
|
let cal_course = CalendarCourse {
|
|
crn: course.crn.clone(),
|
|
subject: course.subject.clone(),
|
|
course_number: course.course_number.clone(),
|
|
title: course.title.clone(),
|
|
sequence_number: course.sequence_number.clone(),
|
|
primary_instructor,
|
|
};
|
|
|
|
Ok((cal_course, meeting_times))
|
|
}
|
|
|
|
/// `GET /api/courses/{term}/{crn}/calendar.ics`
|
|
///
|
|
/// Returns an ICS file download for the course.
|
|
pub async fn course_ics(
|
|
State(state): State<AppState>,
|
|
Path((term, crn)): Path<(String, String)>,
|
|
) -> Result<Response, (StatusCode, String)> {
|
|
let (cal_course, meeting_times) = load_calendar_course(&state, &term, &crn).await?;
|
|
|
|
if meeting_times.is_empty() {
|
|
return Err((
|
|
StatusCode::NOT_FOUND,
|
|
"No meeting times found for this course".to_string(),
|
|
));
|
|
}
|
|
|
|
let result = generate_ics(&cal_course, &meeting_times).map_err(|e| {
|
|
tracing::error!(error = %e, "ICS generation failed");
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Failed to generate ICS file".to_string(),
|
|
)
|
|
})?;
|
|
|
|
let response = (
|
|
[
|
|
(header::CONTENT_TYPE, "text/calendar; charset=utf-8"),
|
|
(
|
|
header::CONTENT_DISPOSITION,
|
|
&format!("attachment; filename=\"{}\"", result.filename),
|
|
),
|
|
(header::CACHE_CONTROL, "no-cache"),
|
|
],
|
|
result.content,
|
|
)
|
|
.into_response();
|
|
|
|
Ok(response)
|
|
}
|
|
|
|
/// `GET /api/courses/{term}/{crn}/gcal`
|
|
///
|
|
/// Redirects to Google Calendar with a pre-filled event for the first meeting time.
|
|
/// If multiple meeting times exist, uses the first one with scheduled days/times.
|
|
pub async fn course_gcal(
|
|
State(state): State<AppState>,
|
|
Path((term, crn)): Path<(String, String)>,
|
|
) -> Result<Response, (StatusCode, String)> {
|
|
let (cal_course, meeting_times) = load_calendar_course(&state, &term, &crn).await?;
|
|
|
|
if meeting_times.is_empty() {
|
|
return Err((
|
|
StatusCode::NOT_FOUND,
|
|
"No meeting times found for this course".to_string(),
|
|
));
|
|
}
|
|
|
|
// Prefer the first meeting time that has actual days/times scheduled
|
|
let mt = meeting_times
|
|
.iter()
|
|
.find(|mt| {
|
|
mt.begin_time.is_some()
|
|
&& (mt.monday
|
|
|| mt.tuesday
|
|
|| mt.wednesday
|
|
|| mt.thursday
|
|
|| mt.friday
|
|
|| mt.saturday
|
|
|| mt.sunday)
|
|
})
|
|
.unwrap_or(&meeting_times[0]);
|
|
|
|
let url = generate_gcal_url(&cal_course, mt).map_err(|e| {
|
|
tracing::error!(error = %e, "Google Calendar URL generation failed");
|
|
(
|
|
StatusCode::INTERNAL_SERVER_ERROR,
|
|
"Failed to generate Google Calendar URL".to_string(),
|
|
)
|
|
})?;
|
|
|
|
Ok(Redirect::temporary(&url).into_response())
|
|
}
|