db/models/
user_module_role.rs

1use sea_orm::entity::prelude::*;
2use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, QueryFilter, Set, DeleteResult};
3use strum::{Display, EnumString};
4use serde::{Deserialize, Serialize};
5
6/// The central table for user-module-role relationships.
7/// Replaces old `module_lecturers`, `module_tutors`, and `module_students`.
8#[derive(Clone, Debug, PartialEq, DeriveEntityModel)]
9#[sea_orm(table_name = "user_module_roles")]
10pub struct Model {
11    /// User ID (foreign key to `users`)
12    #[sea_orm(primary_key, auto_increment = false)]
13    pub user_id: i64,
14
15    /// Module ID (foreign key to `modules`)
16    #[sea_orm(primary_key, auto_increment = false)]
17    pub module_id: i64,
18
19    /// Role type: Lecturer, Tutor, or Student
20    pub role: Role,
21}
22
23/// Enum representing user roles within a module.
24/// Backed by a `user_module_role_type` enum in the database.
25#[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum, Display, EnumString, Deserialize, Serialize)]
26#[serde(rename_all = "snake_case")]
27#[sea_orm(rs_type = "String", db_type = "Enum", enum_name = "user_module_role_type")]
28#[strum(serialize_all = "lowercase", ascii_case_insensitive)]
29pub enum Role {
30    #[sea_orm(string_value = "lecturer")]
31    Lecturer,
32
33    #[sea_orm(string_value = "assistant_lecturer")]
34    AssistantLecturer,
35
36    #[sea_orm(string_value = "tutor")]
37    Tutor,
38
39    #[sea_orm(string_value = "student")]
40    Student,
41}
42
43/// Defines relationships for foreign key joins.
44#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
45pub enum Relation {
46    /// Belongs to a user
47    #[sea_orm(
48        belongs_to = "super::user::Entity",
49        from = "Column::UserId",
50        to = "super::user::Column::Id"
51    )]
52    User,
53
54    /// Belongs to a module
55    #[sea_orm(
56        belongs_to = "super::module::Entity",
57        from = "Column::ModuleId",
58        to = "super::module::Column::Id"
59    )]
60    Module,
61}
62
63impl Related<super::module::Entity> for Entity {
64    fn to() -> RelationDef {
65        Relation::Module.def()
66    }
67
68    fn via() -> Option<RelationDef> {
69        None
70    }
71}
72
73/// Enables customization of insert/update behavior for the ActiveModel.
74impl ActiveModelBehavior for ActiveModel {}
75
76/// Additional CRUD logic and utilities for `user_module_roles`.
77impl Model {
78    /// Assign a user to a module with a specific role.
79    pub async fn assign_user_to_module(
80        db: &DatabaseConnection,
81        user_id: i64,
82        module_id: i64,
83        role: Role,
84    ) -> Result<Self, DbErr> {
85        let active = ActiveModel {
86            user_id: Set(user_id),
87            module_id: Set(module_id),
88            role: Set(role),
89        };
90        active.insert(db).await
91    }
92
93    /// Remove a user from a specific module.
94    pub async fn remove_user_from_module(
95        db: &DatabaseConnection,
96        user_id: i64,
97        module_id: i64,
98    ) -> Result<DeleteResult, DbErr> {
99        Entity::delete_many()
100            .filter(Column::UserId.eq(user_id))
101            .filter(Column::ModuleId.eq(module_id))
102            .exec(db)
103            .await
104    }
105
106    /// Get all role assignments.
107    pub async fn get_all(db: &DatabaseConnection) -> Result<Vec<Self>, DbErr> {
108        Entity::find().all(db).await
109    }
110
111    /// Get all users assigned to a module under a specific role.
112    pub async fn get_users_by_module_role(
113        db: &DatabaseConnection,
114        module_id: i32,
115        role: Role,
116    ) -> Result<Vec<Self>, DbErr> {
117        Entity::find()
118            .filter(Column::ModuleId.eq(module_id))
119            .filter(Column::Role.eq(role))
120            .all(db)
121            .await
122    }
123
124    /// Get all modules a user is assigned to under a specific role.
125    pub async fn get_modules_by_user_role(
126        db: &DatabaseConnection,
127        user_id: i32,
128        role: Role,
129    ) -> Result<Vec<Self>, DbErr> {
130        Entity::find()
131            .filter(Column::UserId.eq(user_id))
132            .filter(Column::Role.eq(role))
133            .all(db)
134            .await
135    }
136}
137
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::models::{module, user};
143    use crate::test_utils::setup_test_db;
144
145    #[tokio::test]
146    async fn test_assign_and_get_user_module_role() {
147        let db = setup_test_db().await;
148
149        // Insert fake user and module records manually
150        user::ActiveModel {
151            id: Set(1),
152            username: Set("s123456".to_string()),
153            email: Set("[email protected]".to_string()),
154            password_hash: Set("hash".to_string()),
155            admin: Set(false),
156            ..Default::default()
157        }
158        .insert(&db)
159        .await
160        .unwrap();
161
162        module::ActiveModel {
163            id: Set(1),
164            code: Set("COS301".to_string()),
165            year: Set(2025),
166            description: Set(Some("Capstone".to_string())),
167            credits: Set(30),
168            ..Default::default()
169        }.insert(&db).await.unwrap();
170
171        let assigned = Model::assign_user_to_module(&db, 1, 1, Role::Lecturer).await.unwrap();
172        assert_eq!(assigned.user_id, 1);
173        assert_eq!(assigned.module_id, 1);
174        assert_eq!(assigned.role, Role::Lecturer);
175
176        let fetched = Model::get_users_by_module_role(&db, 1, Role::Lecturer).await.unwrap();
177        assert_eq!(fetched.len(), 1);
178        assert_eq!(fetched[0].user_id, 1);
179    }
180
181    #[tokio::test]
182    async fn test_remove_user_module_role() {
183        let db = setup_test_db().await;
184
185        // Create foreign keys
186        user::ActiveModel {
187            id: Set(2),
188            username: Set("s654321".to_string()),
189            email: Set("[email protected]".to_string()),
190            password_hash: Set("hash".to_string()),
191            admin: Set(false),
192            ..Default::default()
193        }
194        .insert(&db)
195        .await
196        .unwrap();
197
198        module::ActiveModel {
199            id: Set(2),
200            code: Set("COS333".to_string()),
201            year: Set(2025),
202            description: Set(Some("Networks".to_string())),
203            credits: Set(16),
204            ..Default::default()
205        }.insert(&db).await.unwrap();
206
207        Model::assign_user_to_module(&db, 2, 2, Role::Tutor).await.unwrap();
208        let result = Model::remove_user_from_module(&db, 2, 2).await.unwrap();
209        assert_eq!(result.rows_affected, 1);
210    }
211}