studium/src/core/controller/courses.rs
Antoine Pelletier 07d98b64b3 first commit
2026-09-28 12:04:47 +02:00

118 lines
3.9 KiB
Rust

//! Courses and their weekly time slots.
use chrono::NaiveTime;
use thiserror::Error;
use crate::core::{
controller::ControllerError,
models::{
course::{Course, CourseData, CourseId},
slot_tag::SlotTagId,
},
};
/// The timetable shows monday to friday...
const FIRST_WEEKDAY: u8 = 1;
const LAST_WEEKDAY: u8 = 5;
/// ...from 8:00 to 19:00: slots must fit in it
const DAY_START: NaiveTime = NaiveTime::from_hms_opt(8, 0, 0).unwrap();
const DAY_END: NaiveTime = NaiveTime::from_hms_opt(19, 0, 0).unwrap();
impl super::AppController {
pub async fn get_courses(&self) -> Result<Vec<Course>, ControllerError> {
self.db.get_courses().await.map_err(Into::into)
}
pub async fn create_course(&self, course: CourseData) -> Result<Course, ControllerError> {
let course = self.validate_course(course).await?;
self.db.create_course(course).await.map_err(Into::into)
}
pub async fn update_course(
&self,
id: CourseId,
course: CourseData,
) -> Result<Course, ControllerError> {
let course = self.validate_course(course).await?;
self.db.update_course(id, course).await.map_err(Into::into)
}
pub async fn delete_course(&self, id: CourseId) -> Result<(), ControllerError> {
self.db.delete_course(id).await.map_err(Into::into)
}
/// Trims the texts, turns blank optional fields into `None`, and checks the rules
/// of a course and its slots
async fn validate_course(&self, course: CourseData) -> Result<CourseData, ControllerError> {
let name = course.name.trim().to_owned();
if name.is_empty() {
return Err(CoursesControllerError::EmptyName.into());
}
let code = non_blank(course.code);
let teachers: Vec<String> = course
.teachers
.iter()
.map(|teacher| teacher.trim().to_owned())
.filter(|teacher| !teacher.is_empty())
.collect();
if teachers.is_empty() {
return Err(CoursesControllerError::NoTeacher.into());
}
let main_page_url = non_blank(course.main_page_url);
if let Some(url) = &main_page_url
&& !(url.starts_with("http://") || url.starts_with("https://"))
{
return Err(CoursesControllerError::InvalidMainPageUrl.into());
}
let tags = self.db.get_slot_tags().await?;
for slot in &course.slots {
if !(FIRST_WEEKDAY..=LAST_WEEKDAY).contains(&slot.weekday) {
return Err(CoursesControllerError::InvalidWeekday(slot.weekday).into());
}
if slot.start_time >= slot.end_time
|| slot.start_time < DAY_START
|| slot.end_time > DAY_END
{
return Err(CoursesControllerError::InvalidSlotTime.into());
}
if !tags.iter().any(|tag| tag.id == slot.tag_id) {
return Err(CoursesControllerError::UnknownSlotTag(slot.tag_id).into());
}
}
Ok(CourseData {
name,
code,
teachers,
main_page_url,
slots: course.slots,
})
}
}
fn non_blank(value: Option<String>) -> Option<String> {
value
.map(|value| value.trim().to_owned())
.filter(|value| !value.is_empty())
}
/// Errors specific to this domain area, turned into status codes by the api
#[derive(Error, Debug)]
pub enum CoursesControllerError {
#[error("The name of the course is empty")]
EmptyName,
#[error("A course needs at least one teacher")]
NoTeacher,
#[error("The main page must be an http(s) url")]
InvalidMainPageUrl,
#[error("Invalid weekday {0}: slots must be from monday (1) to friday (5)")]
InvalidWeekday(u8),
#[error("A slot must start before it ends, between 8:00 and 19:00")]
InvalidSlotTime,
#[error("Unknown slot tag {0}")]
UnknownSlotTag(SlotTagId),
}