Skip to content

Commit 0d4b109

Browse files
authored
Merge pull request #141 from MathisBurger/feature/conditional-join-requests
Join Request Policy
2 parents c3cf84c + 88b674a commit 0d4b109

File tree

23 files changed

+386
-33
lines changed

23 files changed

+386
-33
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
ALTER TABLE groups DROP join_policy;
2+
DROP TYPE join_request_policy;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
CREATE TYPE join_request_policy AS ENUM ('open', 'request', 'closed');
2+
ALTER TABLE groups ADD join_policy join_request_policy NOT NULL DEFAULT 'request';

tasky/src/models/group.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ use diesel::prelude::*;
66
use diesel::{associations::HasTable, dsl::not};
77
use serde::{Deserialize, Serialize};
88

9+
#[derive(diesel_derive_enum::DbEnum, Debug, Clone, Deserialize, Serialize, PartialEq)]
10+
#[ExistingTypePath = "crate::schema::sql_types::JoinRequestPolicy"]
11+
pub enum JoinRequestPolicy {
12+
#[serde(rename = "open")]
13+
Open,
14+
#[serde(rename = "request")]
15+
Request,
16+
#[serde(rename = "closed")]
17+
Closed,
18+
}
19+
920
/// Group entity in the database
1021
#[derive(Queryable, Selectable, AsChangeset, Serialize, Deserialize, Clone)]
1122
#[diesel(table_name = crate::schema::groups)]
@@ -15,6 +26,7 @@ pub struct Group {
1526
pub title: String,
1627
pub members: Vec<Option<i32>>,
1728
pub tutor: i32,
29+
pub join_policy: JoinRequestPolicy,
1830
}
1931

2032
/// Used to create a group in database
@@ -24,6 +36,7 @@ pub struct CreateGroup {
2436
pub title: String,
2537
pub tutor: i32,
2638
pub members: Vec<i32>,
39+
pub join_policy: JoinRequestPolicy,
2740
}
2841

2942
pub struct GroupRepository;

tasky/src/models/group_join_request.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
use super::notification::{CreateNotification, NotificationRepository};
12
use super::{Paginate, PaginatedModel, DB};
23
use crate::models::group::Group;
34
use crate::schema::group_join_requests;
@@ -6,7 +7,7 @@ use diesel::associations::HasTable;
67
use diesel::prelude::*;
78

89
/// The group join request in the database
9-
#[derive(Queryable, Selectable, Identifiable, Associations)]
10+
#[derive(Queryable, Selectable, Identifiable, Associations, Clone)]
1011
#[diesel(belongs_to(Group))]
1112
#[diesel(table_name = group_join_requests)]
1213
pub struct GroupJoinRequest {
@@ -53,6 +54,14 @@ impl GroupJoinRequestRepository {
5354
.expect("Cannot get count") as i32
5455
}
5556

57+
/// Gets all join requests for a user
58+
pub fn get_group_requests_no_pagination(group_id: i32, conn: &mut DB) -> Vec<GroupJoinRequest> {
59+
dsl::group_join_requests
60+
.filter(dsl::group_id.eq(group_id))
61+
.get_results::<GroupJoinRequest>(conn)
62+
.expect("Cannot load all join requests for group")
63+
}
64+
5665
/// Checks if a request exists
5766
pub fn request_exists(group_id: i32, user_id: i32, conn: &mut DB) -> bool {
5867
dsl::group_join_requests
@@ -86,8 +95,23 @@ impl GroupJoinRequestRepository {
8695

8796
/// Deletes a request
8897
pub fn delete_request(req: GroupJoinRequest, conn: &mut DB) {
98+
NotificationRepository::create_notification(
99+
&CreateNotification {
100+
title: "Join request rejected".to_string(),
101+
content: "One of your join requests has been rejected".to_string(),
102+
targeted_users: vec![Some(req.requestor)],
103+
},
104+
conn,
105+
);
89106
diesel::delete(dsl::group_join_requests.filter(dsl::id.eq(req.id)))
90107
.execute(conn)
91108
.expect("Cannot delete request");
92109
}
110+
111+
/// Deletes all group join requests for a specific group
112+
pub fn delete_all_requests_for_group(group_id: i32, conn: &mut DB) {
113+
diesel::delete(dsl::group_join_requests.filter(dsl::group_id.eq(group_id)))
114+
.execute(conn)
115+
.expect("Cannot delete request");
116+
}
93117
}

tasky/src/response/group.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::api::usernator_api_client::UsernatorApiClient;
22
use crate::error::ApiError;
3+
use crate::models::group::JoinRequestPolicy;
34
use crate::models::group_join_request::GroupJoinRequestRepository;
45
use crate::models::PaginatedModel;
56
use crate::{api::UserRequest, api::UsersRequest, models::group::Group, response::shared::User};
@@ -16,6 +17,7 @@ pub struct GroupResponse {
1617
pub members: Vec<User>,
1718
pub tutor: User,
1819
pub request_count: i32,
20+
pub join_policy: JoinRequestPolicy,
1921
}
2022

2123
/// The minified group response
@@ -25,6 +27,7 @@ pub struct MinifiedGroupResponse {
2527
pub title: String,
2628
pub member_count: i32,
2729
pub tutor: User,
30+
pub join_policy: JoinRequestPolicy,
2831
}
2932

3033
/// The groups response
@@ -52,6 +55,7 @@ impl Enrich<Group> for MinifiedGroupResponse {
5255
title: from.title.clone(),
5356
member_count: from.members.len() as i32,
5457
tutor: tut.into_inner().into(),
58+
join_policy: from.join_policy.clone(),
5559
})
5660
}
5761
}
@@ -109,6 +113,7 @@ impl Enrich<Group> for GroupResponse {
109113
.map(|x| x.into())
110114
.collect(),
111115
tutor: tut.into_inner().into(),
116+
join_policy: from.join_policy.clone(),
112117
request_count,
113118
})
114119
}

tasky/src/routes/group.rs

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use crate::api::SearchStudentsRequest;
44
use crate::api::UserRequest;
55
use crate::auth_middleware::UserData;
66
use crate::error::ApiError;
7-
use crate::models::group::{CreateGroup, GroupRepository};
7+
use crate::models::group::{CreateGroup, GroupRepository, JoinRequestPolicy};
8+
use crate::models::group_join_request::GroupJoinRequestRepository;
89
use crate::response::group::{GroupResponse, GroupsResponse};
910
use crate::response::shared::User;
1011
use crate::response::Enrich;
@@ -19,6 +20,7 @@ use tonic::transport::Channel;
1920
#[derive(Deserialize, Serialize)]
2021
pub struct CreateGroupRequest {
2122
pub title: String,
23+
pub join_policy: JoinRequestPolicy,
2224
}
2325

2426
/// Endpoint to create a new group
@@ -39,6 +41,7 @@ pub async fn create_group(
3941
title: (req.title).clone(),
4042
tutor: user.user_id,
4143
members: vec![],
44+
join_policy: req.join_policy.clone(),
4245
};
4346
if !new_group.is_granted(SecurityAction::Create, &user) {
4447
return Err(ApiError::Forbidden {
@@ -112,6 +115,60 @@ pub async fn get_group(
112115
})
113116
}
114117

118+
#[derive(Deserialize)]
119+
struct UpdateGroupRequest {
120+
pub title: String,
121+
pub join_policy: JoinRequestPolicy,
122+
}
123+
124+
#[post("/groups/{id}")]
125+
pub async fn update_group(
126+
data: web::Data<AppState>,
127+
user: web::ReqData<UserData>,
128+
path: web::Path<(i32,)>,
129+
req: web::Json<UpdateGroupRequest>,
130+
) -> Result<HttpResponse, ApiError> {
131+
let conn = &mut data.db.db.get().unwrap();
132+
let mut group =
133+
GroupRepository::get_by_id(path.into_inner().0, conn).ok_or(ApiError::BadRequest {
134+
message: "No access to group".to_string(),
135+
})?;
136+
137+
if !group.is_granted(SecurityAction::Update, &user) {
138+
return Err(ApiError::Forbidden {
139+
message: "You are not allowed to update group".to_string(),
140+
});
141+
}
142+
143+
let found_group = GroupRepository::get_by_title(&req.title, conn);
144+
if found_group.is_some() && group.title.clone() != found_group.unwrap().title {
145+
return Err(ApiError::BadRequest {
146+
message: "Group with this name already exists".to_string(),
147+
});
148+
}
149+
150+
group.title = req.title.clone();
151+
group.join_policy = req.join_policy.clone();
152+
153+
if group.join_policy == JoinRequestPolicy::Open {
154+
let requests = GroupJoinRequestRepository::get_group_requests_no_pagination(group.id, conn);
155+
group
156+
.members
157+
.extend(requests.iter().map(|r| Some(r.requestor)));
158+
GroupJoinRequestRepository::delete_all_requests_for_group(group.id, conn);
159+
} else if group.join_policy == JoinRequestPolicy::Closed {
160+
let requests = GroupJoinRequestRepository::get_group_requests_no_pagination(group.id, conn);
161+
for join_request in requests.iter() {
162+
GroupJoinRequestRepository::delete_request(join_request.clone(), conn);
163+
}
164+
}
165+
166+
GroupRepository::update_group(group.clone(), conn);
167+
168+
let enriched = GroupResponse::enrich(&group, &mut data.user_api.clone(), conn).await?;
169+
Ok(HttpResponse::Ok().json(enriched))
170+
}
171+
115172
#[derive(Deserialize)]
116173
struct EnlistableQuery {
117174
pub search: String,

tasky/src/routes/group_join_request.rs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use super::PaginationParams;
22
use crate::auth_middleware::UserData;
33
use crate::error::ApiError;
4-
use crate::models::group::GroupRepository;
4+
use crate::models::group::{GroupRepository, JoinRequestPolicy};
55
use crate::models::group_join_request::{CreateGroupJoinRequest, GroupJoinRequestRepository};
6+
use crate::models::notification::{CreateNotification, NotificationRepository};
67
use crate::response::group::GroupResponse;
78
use crate::response::group_join_request::{GroupJoinRequestResponse, GroupJoinRequestsResponse};
89
use crate::response::Enrich;
@@ -19,10 +20,17 @@ pub async fn create_join_request(
1920
) -> Result<HttpResponse, ApiError> {
2021
let conn = &mut data.db.db.get().unwrap();
2122

22-
let group =
23+
let mut group =
2324
GroupRepository::get_by_id(path.into_inner().0, conn).ok_or(ApiError::BadRequest {
2425
message: "Group does not exist".to_string(),
2526
})?;
27+
28+
if group.join_policy == JoinRequestPolicy::Closed {
29+
return Err(ApiError::Forbidden {
30+
message: "Join requests are not allowed for this group".to_string(),
31+
});
32+
}
33+
2634
if !StaticSecurity::is_granted(StaticSecurityAction::IsStudent, &user)
2735
|| group.members.contains(&Some(user.user_id))
2836
{
@@ -31,6 +39,12 @@ pub async fn create_join_request(
3139
});
3240
}
3341

42+
if group.join_policy == JoinRequestPolicy::Open {
43+
group.members.push(Some(user.user_id));
44+
GroupRepository::update_group(group, conn);
45+
return Ok(HttpResponse::Ok().finish());
46+
}
47+
3448
if GroupJoinRequestRepository::request_exists(group.id, user.user_id, conn) {
3549
return Err(ApiError::Forbidden {
3650
message: "User already sent a request".to_string(),
@@ -45,6 +59,15 @@ pub async fn create_join_request(
4559
conn,
4660
);
4761

62+
NotificationRepository::create_notification(
63+
&CreateNotification {
64+
title: "New join request".to_string(),
65+
content: format!("New join request in group {}", group.title.clone()),
66+
targeted_users: vec![Some(group.tutor)],
67+
},
68+
conn,
69+
);
70+
4871
let resp = GroupJoinRequestResponse::enrich(&request, &mut data.user_api.clone(), conn).await?;
4972
Ok(HttpResponse::Ok().json(resp))
5073
}

tasky/src/routes/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ pub fn init_services(cfg: &mut web::ServiceConfig) {
2020
.service(group::get_group)
2121
.service(group::get_all_groups)
2222
.service(group::get_all_my_groups)
23+
.service(group::update_group)
2324
.service(group::get_enlistable_users)
2425
.service(group::enlist_user)
2526
.service(group::remove_user)

tasky/src/schema.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ pub mod sql_types {
44
#[derive(diesel::query_builder::QueryId, serde::Deserialize, diesel::sql_types::SqlType)]
55
#[diesel(postgres_type(name = "assignment_language"))]
66
pub struct AssignmentLanguage;
7+
8+
#[derive(diesel::query_builder::QueryId, serde::Deserialize, diesel::sql_types::SqlType)]
9+
#[diesel(postgres_type(name = "join_request_policy"))]
10+
pub struct JoinRequestPolicy;
711
}
812

913
diesel::table! {
@@ -62,11 +66,15 @@ diesel::table! {
6266
}
6367

6468
diesel::table! {
69+
use diesel::sql_types::*;
70+
use super::sql_types::JoinRequestPolicy;
71+
6572
groups (id) {
6673
id -> Int4,
6774
title -> Varchar,
6875
members -> Array<Nullable<Int4>>,
6976
tutor -> Int4,
77+
join_policy -> JoinRequestPolicy,
7078
}
7179
}
7280

tasky/tests/routes/a_group.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use super::*;
22
use actix_http::StatusCode;
33
use serial_test::serial;
4-
use tasky::routes::group::CreateGroupRequest;
4+
use tasky::{models::group::JoinRequestPolicy, routes::group::CreateGroupRequest};
55

66
#[actix_web::test]
77
#[serial]
@@ -11,6 +11,7 @@ async fn test_a_create_group_as_student() {
1111
.uri("/create_group")
1212
.set_json(CreateGroupRequest {
1313
title: "name123".to_string(),
14+
join_policy: JoinRequestPolicy::Request,
1415
});
1516
req = student(req);
1617
let resp = test::call_service(&app, req.to_request()).await;
@@ -25,6 +26,7 @@ async fn test_b_create_group_as_tutor() {
2526
.uri("/create_group")
2627
.set_json(CreateGroupRequest {
2728
title: "name".to_string(),
29+
join_policy: JoinRequestPolicy::Request,
2830
});
2931
req = tutor(req);
3032
let resp = test::call_service(&app, req.to_request()).await;
@@ -39,6 +41,7 @@ async fn test_c_create_group_as_tutor_duplicate_name() {
3941
.uri("/create_group")
4042
.set_json(CreateGroupRequest {
4143
title: "name".to_string(),
44+
join_policy: JoinRequestPolicy::Request,
4245
});
4346
req = tutor(req);
4447
let resp = test::call_service(&app, req.to_request()).await;
@@ -53,6 +56,7 @@ async fn test_d_create_group_as_admin() {
5356
.uri("/create_group")
5457
.set_json(CreateGroupRequest {
5558
title: "name2".to_string(),
59+
join_policy: JoinRequestPolicy::Request,
5660
});
5761
req = admin(req);
5862
let resp = test::call_service(&app, req.to_request()).await;

0 commit comments

Comments
 (0)