mirror of
https://github.com/Xevion/banner.git
synced 2025-12-11 02:06:37 -06:00
refactor: clean up MeetingScheduleInfo methods and enhance Term season handling
This commit is contained in:
@@ -487,52 +487,6 @@ impl MeetingScheduleInfo {
|
|||||||
|
|
||||||
(start.and_utc(), end.and_utc())
|
(start.and_utc(), end.and_utc())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get formatted time string
|
|
||||||
pub fn time_string(&self) -> String {
|
|
||||||
match &self.time_range {
|
|
||||||
Some(time_range) => format!("{} {}", self.days_string(), time_range.format_12hr()),
|
|
||||||
None => "No Time".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get formatted schedule string
|
|
||||||
pub fn schedule_string(&self) -> String {
|
|
||||||
match self.meeting_type {
|
|
||||||
MeetingType::HybridBlended => {
|
|
||||||
format!(
|
|
||||||
"{}\nHybrid {}",
|
|
||||||
self.time_string(),
|
|
||||||
self.location.to_string()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
MeetingType::OnlineSynchronous => {
|
|
||||||
format!("{}\nOnline Only", self.time_string())
|
|
||||||
}
|
|
||||||
MeetingType::OnlineAsynchronous => "No Time\nOnline Asynchronous".to_string(),
|
|
||||||
MeetingType::OnlineHybrid => {
|
|
||||||
format!("{}\nOnline Partial", self.time_string())
|
|
||||||
}
|
|
||||||
MeetingType::IndependentStudy => "To Be Arranged".to_string(),
|
|
||||||
MeetingType::FaceToFace => {
|
|
||||||
format!("{}\n{}", self.time_string(), self.location.to_string())
|
|
||||||
}
|
|
||||||
MeetingType::Unknown(_) => "Unknown".to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn occurs_on_day(&self, day: DayOfWeek) -> bool {
|
|
||||||
self.days.contains(MeetingDays::from(day))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get meeting duration in minutes
|
|
||||||
pub fn duration_minutes(&self) -> Option<i64> {
|
|
||||||
if let Some(time_range) = &self.time_range {
|
|
||||||
Some(time_range.duration_minutes())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for MeetingScheduleInfo {
|
impl PartialEq for MeetingScheduleInfo {
|
||||||
|
|||||||
@@ -3,6 +3,14 @@ use std::{ops::RangeInclusive, str::FromStr};
|
|||||||
use anyhow::Context;
|
use anyhow::Context;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
/// The current year at the time of compilation
|
||||||
|
const CURRENT_YEAR: u32 = compile_time::date!().year() as u32;
|
||||||
|
|
||||||
|
/// The valid years for terms
|
||||||
|
/// We set a semi-static upper limit to avoid having to update this value while also keeping a tight bound
|
||||||
|
/// TODO: Recheck the lower bound, it's just a guess right now.
|
||||||
|
const VALID_YEARS: RangeInclusive<u32> = 2007..=(CURRENT_YEAR + 10);
|
||||||
|
|
||||||
/// Represents a term in the Banner system
|
/// Represents a term in the Banner system
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Term {
|
pub struct Term {
|
||||||
@@ -18,34 +26,37 @@ pub enum Season {
|
|||||||
Summer,
|
Summer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Term {
|
impl ToString for Term {
|
||||||
pub fn to_string(&self) -> String {
|
/// Returns the term in the format YYYYXX, where YYYY is the year and XX is the season code
|
||||||
format!("{}{}", self.year, self.season.to_string())
|
fn to_string(&self) -> String {
|
||||||
|
format!("{}{}", self.year, self.season.to_str())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Season {
|
impl Season {
|
||||||
pub fn to_string(&self) -> String {
|
/// Returns the season code as a string
|
||||||
(match self {
|
fn to_str(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
Season::Fall => "10",
|
Season::Fall => "10",
|
||||||
Season::Spring => "20",
|
Season::Spring => "20",
|
||||||
Season::Summer => "30",
|
Season::Summer => "30",
|
||||||
})
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_string(s: &str) -> Option<Season> {
|
|
||||||
match s {
|
|
||||||
"10" => Some(Season::Fall),
|
|
||||||
"20" => Some(Season::Spring),
|
|
||||||
"30" => Some(Season::Summer),
|
|
||||||
_ => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CURRENT_YEAR: u32 = compile_time::date!().year() as u32;
|
impl FromStr for Season {
|
||||||
const VALID_YEARS: RangeInclusive<u32> = 2007..=(CURRENT_YEAR + 10);
|
type Err = anyhow::Error;
|
||||||
|
|
||||||
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
|
let season = match s {
|
||||||
|
"10" => Season::Fall,
|
||||||
|
"20" => Season::Spring,
|
||||||
|
"30" => Season::Summer,
|
||||||
|
_ => return Err(anyhow::anyhow!("Invalid season: {}", s)),
|
||||||
|
};
|
||||||
|
Ok(season)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FromStr for Term {
|
impl FromStr for Term {
|
||||||
type Err = anyhow::Error;
|
type Err = anyhow::Error;
|
||||||
@@ -61,16 +72,8 @@ impl FromStr for Term {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let season =
|
let season =
|
||||||
Season::from_string(&s[4..6]).ok_or_else(|| anyhow::anyhow!("Invalid season code"))?;
|
Season::from_str(&s[4..6]).map_err(|e| anyhow::anyhow!("Invalid season: {}", e))?;
|
||||||
|
|
||||||
Ok(Term { year, season })
|
Ok(Term { year, season })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for Season {
|
|
||||||
type Err = ();
|
|
||||||
|
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
||||||
Self::from_string(s).ok_or(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,12 @@
|
|||||||
//! Google Calendar command implementation.
|
//! Google Calendar command implementation.
|
||||||
|
|
||||||
use crate::banner::{Course, MeetingScheduleInfo, TimeRange};
|
use crate::banner::{Course, DayOfWeek, MeetingScheduleInfo};
|
||||||
use crate::bot::{Context, Error};
|
use crate::bot::{Context, Error};
|
||||||
use chrono::NaiveDate;
|
use chrono::NaiveDate;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use tracing::{error, info};
|
use tracing::{error, info};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
const TIMESTAMP_FORMAT: &str = "%Y%m%dT%H%M%SZ";
|
|
||||||
|
|
||||||
/// Generate a link to create a Google Calendar event for a course
|
/// Generate a link to create a Google Calendar event for a course
|
||||||
#[poise::command(slash_command, prefix_command)]
|
#[poise::command(slash_command, prefix_command)]
|
||||||
pub async fn gcal(
|
pub async fn gcal(
|
||||||
@@ -66,7 +64,7 @@ pub async fn gcal(
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Get meeting times
|
// Get meeting times
|
||||||
let mut meeting_times = match banner_api.get_course_meeting_time(term, crn).await {
|
let meeting_times = match banner_api.get_course_meeting_time(term, crn).await {
|
||||||
Ok(meeting_time) => meeting_time,
|
Ok(meeting_time) => meeting_time,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to get meeting times: {}", e);
|
error!("Failed to get meeting times: {}", e);
|
||||||
@@ -87,7 +85,7 @@ pub async fn gcal(
|
|||||||
.map(|m| {
|
.map(|m| {
|
||||||
let link = generate_gcal_url(&course, m)?;
|
let link = generate_gcal_url(&course, m)?;
|
||||||
let detail = match &m.time_range {
|
let detail = match &m.time_range {
|
||||||
Some(range) => range.format_12hr(),
|
Some(range) => format!("{} {}", m.days_string(), range.format_12hr()),
|
||||||
None => m.days_string(),
|
None => m.days_string(),
|
||||||
};
|
};
|
||||||
Ok(LinkDetail { link, detail })
|
Ok(LinkDetail { link, detail })
|
||||||
@@ -175,32 +173,23 @@ fn generate_gcal_url(
|
|||||||
|
|
||||||
/// Generate RRULE for recurrence
|
/// Generate RRULE for recurrence
|
||||||
fn generate_rrule(meeting_time: &MeetingScheduleInfo, end_date: NaiveDate) -> String {
|
fn generate_rrule(meeting_time: &MeetingScheduleInfo, end_date: NaiveDate) -> String {
|
||||||
let by_day = meeting_time.days_string();
|
let days_of_week = meeting_time.days_of_week();
|
||||||
|
let by_day = days_of_week
|
||||||
// Handle edge cases where days_string might return "None" or empty
|
.iter()
|
||||||
let by_day = if by_day.is_empty() || by_day == "None" {
|
.map(|day| match day {
|
||||||
"MO".to_string() // Default to Monday
|
DayOfWeek::Monday => "MO",
|
||||||
} else {
|
DayOfWeek::Tuesday => "TU",
|
||||||
// Convert our day format to Google Calendar format
|
DayOfWeek::Wednesday => "WE",
|
||||||
by_day
|
DayOfWeek::Thursday => "TH",
|
||||||
.replace("M", "MO")
|
DayOfWeek::Friday => "FR",
|
||||||
.replace("Tu", "TU")
|
DayOfWeek::Saturday => "SA",
|
||||||
.replace("W", "WE")
|
DayOfWeek::Sunday => "SU",
|
||||||
.replace("Th", "TH")
|
})
|
||||||
.replace("F", "FR")
|
.collect::<Vec<&str>>()
|
||||||
.replace("Sa", "SA")
|
.join(",");
|
||||||
.replace("Su", "SU")
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format end date for RRULE (YYYYMMDD format)
|
// Format end date for RRULE (YYYYMMDD format)
|
||||||
let until = end_date.format("%Y%m%d").to_string();
|
let until = end_date.format("%Y%m%d").to_string();
|
||||||
|
|
||||||
// Build the RRULE string manually to avoid formatting issues
|
format!("RRULE:FREQ=WEEKLY;BYDAY={by_day};UNTIL={until}")
|
||||||
let mut rrule = String::new();
|
|
||||||
rrule.push_str("FREQ=WEEKLY;BYDAY=");
|
|
||||||
rrule.push_str(&by_day);
|
|
||||||
rrule.push_str(";UNTIL=");
|
|
||||||
rrule.push_str(&until);
|
|
||||||
|
|
||||||
rrule
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user