118 lines
3.9 KiB
Rust
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),
|
|
}
|