Skip to content

Commit a0a45c5

Browse files
authored
Merge pull request #155 from MathisBurger/feature/verified-groups
Verified groups
2 parents fb60bda + f68cd7c commit a0a45c5

File tree

19 files changed

+180
-27
lines changed

19 files changed

+180
-27
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE groups DROP COLUMN verified;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
ALTER TABLE groups ADD COLUMN verified BOOLEAN NOT NULL DEFAULT false;

tasky/src/models/group.rs

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ pub struct Group {
3333
pub join_policy: JoinRequestPolicy,
3434
pub created_at: NaiveDateTime,
3535
pub updated_at: NaiveDateTime,
36+
pub verified: bool,
3637
}
3738

3839
/// Used to create a group in database
@@ -104,15 +105,25 @@ impl GroupRepository {
104105
page: i64,
105106
conn: &mut DB,
106107
) -> PaginatedModel<Group> {
107-
dsl::groups
108+
let result = dsl::groups
108109
.filter(
109110
dsl::tutor
110111
.eq(member_id)
111112
.or(dsl::members.contains(vec![Some(member_id)])),
112113
)
114+
.group_by((dsl::id, dsl::verified))
115+
.order(dsl::verified.desc())
113116
.paginate(page)
114-
.load_and_count_pages::<Group>(conn)
115-
.expect("Cannot fetch groups for member")
117+
.load_and_count_pages::<Group>(conn);
118+
if let Ok(model) = result {
119+
model
120+
} else {
121+
PaginatedModel {
122+
results: vec![],
123+
total: 0,
124+
page,
125+
}
126+
}
116127
}
117128

118129
/// Gets all groups a user is member or tutor of
@@ -139,16 +150,25 @@ impl GroupRepository {
139150
.expect("Result cannot be fetched");
140151

141152
let results = dsl::groups
153+
.group_by((dsl::id, dsl::verified))
142154
.into_boxed()
143155
.filter(apply_search_filter(member_id, requested, search))
156+
.order(dsl::verified.desc())
144157
.limit(50)
145158
.offset((page - 1) * 50)
146-
.load::<Group>(conn)
147-
.expect("Result cannot be fetched");
159+
.load::<Group>(conn);
160+
161+
if results.is_err() {
162+
return PaginatedModel {
163+
total: 0,
164+
results: vec![],
165+
page,
166+
};
167+
}
148168

149169
PaginatedModel {
150170
total,
151-
results,
171+
results: results.unwrap(),
152172
page,
153173
}
154174
}

tasky/src/response/group.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub struct GroupResponse {
1818
pub tutor: User,
1919
pub request_count: i32,
2020
pub join_policy: JoinRequestPolicy,
21+
pub verified: bool,
2122
}
2223

2324
/// The minified group response
@@ -28,6 +29,7 @@ pub struct MinifiedGroupResponse {
2829
pub member_count: i32,
2930
pub tutor: User,
3031
pub join_policy: JoinRequestPolicy,
32+
pub verified: bool,
3133
}
3234

3335
/// The groups response
@@ -56,6 +58,7 @@ impl Enrich<Group> for MinifiedGroupResponse {
5658
member_count: from.members.len() as i32,
5759
tutor: tut.into_inner().into(),
5860
join_policy: from.join_policy.clone(),
61+
verified: from.verified,
5962
})
6063
}
6164
}
@@ -114,6 +117,7 @@ impl Enrich<Group> for GroupResponse {
114117
.collect(),
115118
tutor: tut.into_inner().into(),
116119
join_policy: from.join_policy.clone(),
120+
verified: from.verified,
117121
request_count,
118122
})
119123
}

tasky/src/routes/group.rs

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,3 +355,51 @@ pub async fn delete_group(
355355

356356
Ok(HttpResponse::Ok().finish())
357357
}
358+
359+
/// Endpoint to verify a group
360+
#[post("/groups/{id}/verify")]
361+
pub async fn verify_group(
362+
data: web::Data<AppState>,
363+
user: web::ReqData<UserData>,
364+
path: web::Path<(i32,)>,
365+
) -> Result<HttpResponse, ApiError> {
366+
let conn = &mut data.db.db.get().unwrap();
367+
let path_data = path.into_inner();
368+
369+
let mut group = GroupRepository::get_by_id(path_data.0, conn).ok_or(ApiError::BadRequest {
370+
message: "No access to group".to_string(),
371+
})?;
372+
373+
if !StaticSecurity::is_granted(crate::security::StaticSecurityAction::IsAdmin, &user) {
374+
return Err(ApiError::Forbidden {
375+
message: "Only admins are allowed to verify groups".to_string(),
376+
});
377+
}
378+
group.verified = true;
379+
GroupRepository::update_group(group, conn);
380+
Ok(HttpResponse::Ok().finish())
381+
}
382+
383+
/// Endpoint to unverify a group
384+
#[post("/groups/{id}/unverify")]
385+
pub async fn unverify_group(
386+
data: web::Data<AppState>,
387+
user: web::ReqData<UserData>,
388+
path: web::Path<(i32,)>,
389+
) -> Result<HttpResponse, ApiError> {
390+
let conn = &mut data.db.db.get().unwrap();
391+
let path_data = path.into_inner();
392+
393+
let mut group = GroupRepository::get_by_id(path_data.0, conn).ok_or(ApiError::BadRequest {
394+
message: "No access to group".to_string(),
395+
})?;
396+
397+
if !StaticSecurity::is_granted(crate::security::StaticSecurityAction::IsAdmin, &user) {
398+
return Err(ApiError::Forbidden {
399+
message: "Only admins are allowed to unverify groups".to_string(),
400+
});
401+
}
402+
group.verified = false;
403+
GroupRepository::update_group(group, conn);
404+
Ok(HttpResponse::Ok().finish())
405+
}

tasky/src/routes/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ pub fn init_services(cfg: &mut web::ServiceConfig) {
3232
.service(group::remove_user)
3333
.service(group::leave_group)
3434
.service(group::delete_group)
35+
.service(group::verify_group)
36+
.service(group::unverify_group)
3537
.service(group_join_request::create_join_request)
3638
.service(group_join_request::get_join_requests)
3739
.service(group_join_request::approve_join_request)

tasky/src/schema.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@ diesel::table! {
8585
join_policy -> JoinRequestPolicy,
8686
created_at -> Timestamp,
8787
updated_at -> Timestamp,
88+
verified -> Bool,
8889
}
8990
}
9091

tasky/tests/security/group_test.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ fn test_create_group() {
2424
.unwrap(),
2525
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
2626
.unwrap(),
27+
verified: false,
2728
};
2829
assert_eq!(group.is_granted(SecurityAction::Create, &admin), false);
2930
}
@@ -41,6 +42,7 @@ fn test_read_group_as_admin() {
4142
.unwrap(),
4243
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
4344
.unwrap(),
45+
verified: false,
4446
};
4547
assert_eq!(group.is_granted(SecurityAction::Read, &admin), true);
4648
}
@@ -58,6 +60,7 @@ fn test_read_group_as_tutor() {
5860
.unwrap(),
5961
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
6062
.unwrap(),
63+
verified: false,
6164
};
6265
assert_eq!(group.is_granted(SecurityAction::Read, &admin), true);
6366
}
@@ -75,6 +78,7 @@ fn test_read_group_as_wrong_tutor() {
7578
.unwrap(),
7679
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
7780
.unwrap(),
81+
verified: false,
7882
};
7983
assert_eq!(group.is_granted(SecurityAction::Read, &admin), false);
8084
}
@@ -92,6 +96,7 @@ fn test_read_group_as_student() {
9296
.unwrap(),
9397
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
9498
.unwrap(),
99+
verified: false,
95100
};
96101
assert_eq!(group.is_granted(SecurityAction::Read, &admin), true);
97102
}
@@ -109,6 +114,7 @@ fn test_read_group_as_wrong_student() {
109114
.unwrap(),
110115
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
111116
.unwrap(),
117+
verified: false,
112118
};
113119
assert_eq!(group.is_granted(SecurityAction::Read, &admin), false);
114120
}
@@ -126,6 +132,7 @@ fn test_update_as_admin() {
126132
.unwrap(),
127133
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
128134
.unwrap(),
135+
verified: false,
129136
};
130137
assert_eq!(group.is_granted(SecurityAction::Update, &user), true);
131138
}
@@ -143,6 +150,7 @@ fn test_update_as_tutor() {
143150
.unwrap(),
144151
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
145152
.unwrap(),
153+
verified: false,
146154
};
147155
assert_eq!(group.is_granted(SecurityAction::Update, &user), true);
148156
}
@@ -160,6 +168,7 @@ fn test_update_as_wrong_tutor() {
160168
.unwrap(),
161169
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
162170
.unwrap(),
171+
verified: false,
163172
};
164173
assert_eq!(group.is_granted(SecurityAction::Update, &user), false);
165174
}
@@ -177,6 +186,7 @@ fn test_update_as_student() {
177186
.unwrap(),
178187
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
179188
.unwrap(),
189+
verified: false,
180190
};
181191
assert_eq!(group.is_granted(SecurityAction::Update, &user), false);
182192
}
@@ -194,6 +204,7 @@ fn test_delete_as_admin() {
194204
.unwrap(),
195205
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
196206
.unwrap(),
207+
verified: false,
197208
};
198209
assert_eq!(group.is_granted(SecurityAction::Delete, &user), false);
199210
}
@@ -211,6 +222,7 @@ fn test_delete_as_tutor() {
211222
.unwrap(),
212223
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
213224
.unwrap(),
225+
verified: false,
214226
};
215227
assert_eq!(group.is_granted(SecurityAction::Delete, &user), false);
216228
}
@@ -228,6 +240,7 @@ fn test_delete_as_wrong_tutor() {
228240
.unwrap(),
229241
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
230242
.unwrap(),
243+
verified: false,
231244
};
232245
assert_eq!(group.is_granted(SecurityAction::Delete, &user), false);
233246
}
@@ -245,6 +258,7 @@ fn test_delete_as_student() {
245258
.unwrap(),
246259
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
247260
.unwrap(),
261+
verified: false,
248262
};
249263
assert_eq!(group.is_granted(SecurityAction::Delete, &user), false);
250264
}

web/app/dashboard/page.tsx

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,16 @@ const DashboardPage = () => {
3232
</Grid.Col>
3333
<Grid.Col span={8}>
3434
<Card shadow="sm" padding="xl" mt={20}>
35-
<Title order={2}>Release v0.2.1</Title>
35+
<Title order={2}>Release v0.2.2</Title>
3636
<Text>
3737
We had some groundbreaking changes within our app for the current
3838
release:
3939
<br />
40-
- JavaFX support <br/>
40+
- Verified groups <br/>
41+
- Group leaving and deletion <br/>
42+
- Group join policy feature update <br/>
4143
- Notification system <br/>
42-
- Stage3 Spotlight <br/>
43-
- Assignment test editing <br/>
44-
- Completion indicator on assignments <br/>
45-
- Some more backlinks <br/>
46-
- Big performance improvements for web <br/>
47-
- Minor bug fixes
44+
- Convert to tutor account <br/>
4845
</Text>
4946
</Card>
5047
</Grid.Col>

web/app/groups/[groupId]/page.tsx

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {isGranted} from "@/service/auth";
1515
import {UserRoles} from "@/service/types/usernator";
1616
import LeaveGroupModal from "@/components/group/LeaveGroupModal";
1717
import DeleteGroupModal from "@/components/group/DeleteGroupModal";
18+
import VerifiedBadge from "@/components/VerifiedBadge";
19+
import NavigateBack from "@/components/NavigateBack";
1820

1921
const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
2022
const id = parseInt(`${params.groupId}`, 10);
@@ -27,6 +29,17 @@ const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
2729
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
2830
const { t } = useTranslation("common");
2931

32+
const changeVerifiedState = async () => {
33+
if (group) {
34+
if (group.verified) {
35+
await api.unverify(group.id);
36+
} else {
37+
await api.verify(group.id);
38+
}
39+
refetch();
40+
}
41+
}
42+
3043
useEffect(() => {
3144
if (group) {
3245
addGroup(group);
@@ -43,12 +56,16 @@ const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
4356

4457
return (
4558
<Container fluid>
59+
<NavigateBack />
4660
<Group>
4761
<Title>{group?.title ?? "Loading"}</Title>
4862
<Badge>{group?.tutor?.username ?? "Loading"}</Badge>
4963
{group?.join_policy && (
5064
<GroupJoinPolicyBadge policy={group.join_policy} />
5165
)}
66+
{group?.verified && (
67+
<VerifiedBadge />
68+
)}
5269
{(isGranted(user, [UserRoles.Admin]) || group?.tutor.id === user?.id) && (
5370
<>
5471
<Button onClick={() => setUpdateModalOpen(true)}>{t('common:titles.update-group')}</Button>
@@ -58,6 +75,9 @@ const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
5875
{isGranted(user, [UserRoles.Student]) && (
5976
<Button color="red" onClick={() => setLeaveModalOpen(true)}>{t('group:actions.leave')}</Button>
6077
)}
78+
{isGranted(user, [UserRoles.Admin]) && (
79+
<Button color="cyan" onClick={changeVerifiedState}>{group?.verified ? t('group:actions.unverify') : t('group:actions.verify')}</Button>
80+
)}
6181
</Group>
6282
{group === null ? (
6383
<CentralLoading />

web/app/groups/displayComponent.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import useCurrentUser from "@/hooks/useCurrentUser";
1212
import { isGranted } from "@/service/auth";
1313
import { useTranslation } from "react-i18next";
1414
import GroupJoinPolicyBadge from "@/components/group/GroupJoinPolicyBadge";
15+
import VerifiedBadge from "@/components/VerifiedBadge";
1516

1617
interface DisplayComponentProps {
1718
groups: MinifiedGroup[];
@@ -34,6 +35,9 @@ const GroupsDisplayComponent = ({
3435
{
3536
field: "title",
3637
label: t("group:cols.title"),
38+
render: (title, row) => (
39+
<p>{title}&nbsp;{row.verified ? <VerifiedBadge /> : null} </p>
40+
)
3741
},
3842
{
3943
field: "member_count",

0 commit comments

Comments
 (0)