db/models/
module.rs

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    /// Create a new module record in the database.
46    ///
47    /// # Arguments
48    /// * `db` - Reference to the database connection.
49    /// * `code` - The module code (e.g., "COS301").
50    /// * `year` - The academic year.
51    /// * `description` - Optional module description.
52    /// * `credits` - Credit value for the module.
53    ///
54    /// # Returns
55    /// A fully populated `Model` after insertion.
56    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    /// Deletes this module, its assignments, and associated files/folders.
78    pub async fn delete(self, db: &DatabaseConnection) -> Result<(), DbErr> {
79        // Step 1: Let DB cascade delete assignments
80        info!("Deleting module {} and cascading assignments", self.id);
81
82        // Step 2: Remove module-level folder
83        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        // Step 3: Delete the module
98        Entity::delete_by_id(self.id).exec(db).await?;
99        info!("Deleted module {}", self.id);
100
101        Ok(())
102    }
103
104    /// Edit a module by ID and return the updated model.
105    ///
106    /// All fields will be updated, and `updated_at` will be auto-set by the DB.
107    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}