1use sea_orm::entity::prelude::*;
2use sea_orm::{DatabaseConnection, EntityTrait, ActiveModelTrait, Set};
3use chrono::{DateTime, Utc};
4use log::{info, warn};
5use std::path::PathBuf;
6use std::fs;
7use std::env;
8
9#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
10#[sea_orm(table_name = "modules")]
11pub struct Model {
12 #[sea_orm(primary_key)]
13 pub id: i64,
14 pub code: String,
15 pub year: i32,
16 pub description: Option<String>,
17 pub credits: i32,
18 pub created_at: DateTime<Utc>,
19 pub updated_at: DateTime<Utc>,
20}
21
22#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
23pub enum Relation {
24 #[sea_orm(
25 has_many = "super::user_module_role::Entity",
26 from = "Column::Id",
27 to = "super::user_module_role::Column::ModuleId"
28 )]
29 UserModuleRole,
30}
31
32impl Related<super::user_module_role::Entity> for Entity {
33 fn to() -> RelationDef {
34 Relation::UserModuleRole.def()
35 }
36
37 fn via() -> Option<RelationDef> {
38 None
39 }
40}
41
42impl ActiveModelBehavior for ActiveModel {}
43
44impl Model {
45 pub async fn create<C>(
57 db: &C,
58 code: &str,
59 year: i32,
60 description: Option<&str>,
61 credits: i32,
62 ) -> Result<Self, DbErr>
63 where
64 C: ConnectionTrait,
65 {
66 let active = ActiveModel {
67 code: Set(code.to_owned()),
68 year: Set(year),
69 description: Set(description.map(|d| d.to_owned())),
70 credits: Set(credits),
71 ..Default::default()
72 };
73
74 active.insert(db).await
75 }
76
77 pub async fn delete(self, db: &DatabaseConnection) -> Result<(), DbErr> {
79 info!("Deleting module {} and cascading assignments", self.id);
81
82 let storage_root = env::var("ASSIGNMENT_STORAGE_ROOT")
84 .unwrap_or_else(|_| "data/assignment_files".to_string());
85
86 let module_dir = PathBuf::from(storage_root).join(format!("module_{}", self.id));
87
88 if module_dir.exists() {
89 match fs::remove_dir_all(&module_dir) {
90 Ok(_) => info!("Deleted module directory {}", module_dir.display()),
91 Err(e) => warn!("Failed to delete module directory {}: {}", module_dir.display(), e),
92 }
93 } else {
94 warn!("Expected module directory {} does not exist", module_dir.display());
95 }
96
97 Entity::delete_by_id(self.id).exec(db).await?;
99 info!("Deleted module {}", self.id);
100
101 Ok(())
102 }
103
104 pub async fn edit(
108 db: &DatabaseConnection,
109 id: i64,
110 code: &str,
111 year: i32,
112 description: Option<&str>,
113 credits: i32,
114 ) -> Result<Self, DbErr> {
115 let Some(module) = Entity::find_by_id(id).one(db).await? else {
116 return Err(DbErr::RecordNotFound(format!("Module ID {} not found", id)));
117 };
118
119 let mut active: ActiveModel = module.into();
120 active.code = Set(code.to_owned());
121 active.year = Set(year);
122 active.description = Set(description.map(|d| d.to_owned()));
123 active.credits = Set(credits);
124
125 active.update(db).await
126 }
127}
128
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::test_utils::setup_test_db;
134
135 #[tokio::test]
136 async fn test_create_module() {
137 let db = setup_test_db().await;
138
139 let code = "COS301";
140 let year = 2025;
141 let description = Some("Software Engineering");
142 let credits = 16;
143
144 let module = Model::create(&db, code, year, description, credits)
145 .await
146 .expect("Failed to create module");
147
148 assert_eq!(module.code, code);
149 assert_eq!(module.year, year);
150 assert_eq!(module.description.as_deref(), description);
151 assert_eq!(module.credits, credits);
152 }
153
154 #[tokio::test]
155 async fn test_edit_module() {
156 let db = setup_test_db().await;
157
158 let initial = Model::create(&db, "COS132", 2024, Some("Old Desc"), 12)
159 .await
160 .unwrap();
161
162 let updated = Model::edit(&db, initial.id, "COS133", 2025, Some("New Desc"), 14)
163 .await
164 .expect("Failed to edit module");
165
166 assert_eq!(updated.id, initial.id);
167 assert_eq!(updated.code, "COS133");
168 assert_eq!(updated.year, 2025);
169 assert_eq!(updated.description.as_deref(), Some("New Desc"));
170 assert_eq!(updated.credits, 14);
171 }
172}