Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Verified groups #155

Merged
merged 11 commits into from
Nov 18, 2024
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE groups DROP COLUMN verified;
1 change: 1 addition & 0 deletions tasky/migrations/2024-11-18-175123_verified_groups/up.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE groups ADD COLUMN verified BOOLEAN NOT NULL DEFAULT false;
32 changes: 26 additions & 6 deletions tasky/src/models/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ pub struct Group {
pub join_policy: JoinRequestPolicy,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub verified: bool,
}

/// Used to create a group in database
Expand Down Expand Up @@ -104,15 +105,25 @@ impl GroupRepository {
page: i64,
conn: &mut DB,
) -> PaginatedModel<Group> {
dsl::groups
let result = dsl::groups
.filter(
dsl::tutor
.eq(member_id)
.or(dsl::members.contains(vec![Some(member_id)])),
)
.group_by((dsl::id, dsl::verified))
.order(dsl::verified.desc())
.paginate(page)
.load_and_count_pages::<Group>(conn)
.expect("Cannot fetch groups for member")
.load_and_count_pages::<Group>(conn);
if let Ok(model) = result {
model
} else {
PaginatedModel {
results: vec![],
total: 0,
page,
}
}
}

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

let results = dsl::groups
.group_by((dsl::id, dsl::verified))
.into_boxed()
.filter(apply_search_filter(member_id, requested, search))
.order(dsl::verified.desc())
.limit(50)
.offset((page - 1) * 50)
.load::<Group>(conn)
.expect("Result cannot be fetched");
.load::<Group>(conn);

if results.is_err() {
return PaginatedModel {
total: 0,
results: vec![],
page,
};
}

PaginatedModel {
total,
results,
results: results.unwrap(),
page,
}
}
Expand Down
4 changes: 4 additions & 0 deletions tasky/src/response/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct GroupResponse {
pub tutor: User,
pub request_count: i32,
pub join_policy: JoinRequestPolicy,
pub verified: bool,
}

/// The minified group response
Expand All @@ -28,6 +29,7 @@ pub struct MinifiedGroupResponse {
pub member_count: i32,
pub tutor: User,
pub join_policy: JoinRequestPolicy,
pub verified: bool,
}

/// The groups response
Expand Down Expand Up @@ -56,6 +58,7 @@ impl Enrich<Group> for MinifiedGroupResponse {
member_count: from.members.len() as i32,
tutor: tut.into_inner().into(),
join_policy: from.join_policy.clone(),
verified: from.verified,
})
}
}
Expand Down Expand Up @@ -114,6 +117,7 @@ impl Enrich<Group> for GroupResponse {
.collect(),
tutor: tut.into_inner().into(),
join_policy: from.join_policy.clone(),
verified: from.verified,
request_count,
})
}
Expand Down
48 changes: 48 additions & 0 deletions tasky/src/routes/group.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,3 +355,51 @@ pub async fn delete_group(

Ok(HttpResponse::Ok().finish())
}

/// Endpoint to verify a group
#[post("/groups/{id}/verify")]
pub async fn verify_group(
data: web::Data<AppState>,
user: web::ReqData<UserData>,
path: web::Path<(i32,)>,
) -> Result<HttpResponse, ApiError> {
let conn = &mut data.db.db.get().unwrap();
let path_data = path.into_inner();

let mut group = GroupRepository::get_by_id(path_data.0, conn).ok_or(ApiError::BadRequest {
message: "No access to group".to_string(),
})?;

if !StaticSecurity::is_granted(crate::security::StaticSecurityAction::IsAdmin, &user) {
return Err(ApiError::Forbidden {
message: "Only admins are allowed to verify groups".to_string(),
});
}
group.verified = true;
GroupRepository::update_group(group, conn);
Ok(HttpResponse::Ok().finish())
}

/// Endpoint to unverify a group
#[post("/groups/{id}/unverify")]
pub async fn unverify_group(
data: web::Data<AppState>,
user: web::ReqData<UserData>,
path: web::Path<(i32,)>,
) -> Result<HttpResponse, ApiError> {
let conn = &mut data.db.db.get().unwrap();
let path_data = path.into_inner();

let mut group = GroupRepository::get_by_id(path_data.0, conn).ok_or(ApiError::BadRequest {
message: "No access to group".to_string(),
})?;

if !StaticSecurity::is_granted(crate::security::StaticSecurityAction::IsAdmin, &user) {
return Err(ApiError::Forbidden {
message: "Only admins are allowed to unverify groups".to_string(),
});
}
group.verified = false;
GroupRepository::update_group(group, conn);
Ok(HttpResponse::Ok().finish())
}
2 changes: 2 additions & 0 deletions tasky/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub fn init_services(cfg: &mut web::ServiceConfig) {
.service(group::remove_user)
.service(group::leave_group)
.service(group::delete_group)
.service(group::verify_group)
.service(group::unverify_group)
.service(group_join_request::create_join_request)
.service(group_join_request::get_join_requests)
.service(group_join_request::approve_join_request)
Expand Down
1 change: 1 addition & 0 deletions tasky/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ diesel::table! {
join_policy -> JoinRequestPolicy,
created_at -> Timestamp,
updated_at -> Timestamp,
verified -> Bool,
}
}

Expand Down
14 changes: 14 additions & 0 deletions tasky/tests/security/group_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ fn test_create_group() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Create, &admin), false);
}
Expand All @@ -41,6 +42,7 @@ fn test_read_group_as_admin() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Read, &admin), true);
}
Expand All @@ -58,6 +60,7 @@ fn test_read_group_as_tutor() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Read, &admin), true);
}
Expand All @@ -75,6 +78,7 @@ fn test_read_group_as_wrong_tutor() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Read, &admin), false);
}
Expand All @@ -92,6 +96,7 @@ fn test_read_group_as_student() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Read, &admin), true);
}
Expand All @@ -109,6 +114,7 @@ fn test_read_group_as_wrong_student() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Read, &admin), false);
}
Expand All @@ -126,6 +132,7 @@ fn test_update_as_admin() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Update, &user), true);
}
Expand All @@ -143,6 +150,7 @@ fn test_update_as_tutor() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Update, &user), true);
}
Expand All @@ -160,6 +168,7 @@ fn test_update_as_wrong_tutor() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Update, &user), false);
}
Expand All @@ -177,6 +186,7 @@ fn test_update_as_student() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Update, &user), false);
}
Expand All @@ -194,6 +204,7 @@ fn test_delete_as_admin() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Delete, &user), false);
}
Expand All @@ -211,6 +222,7 @@ fn test_delete_as_tutor() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Delete, &user), false);
}
Expand All @@ -228,6 +240,7 @@ fn test_delete_as_wrong_tutor() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Delete, &user), false);
}
Expand All @@ -245,6 +258,7 @@ fn test_delete_as_student() {
.unwrap(),
updated_at: NaiveDateTime::parse_from_str("2015-09-05 23:56:04", "%Y-%m-%d %H:%M:%S")
.unwrap(),
verified: false,
};
assert_eq!(group.is_granted(SecurityAction::Delete, &user), false);
}
Expand Down
13 changes: 5 additions & 8 deletions web/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,16 @@ const DashboardPage = () => {
</Grid.Col>
<Grid.Col span={8}>
<Card shadow="sm" padding="xl" mt={20}>
<Title order={2}>Release v0.2.1</Title>
<Title order={2}>Release v0.2.2</Title>
<Text>
We had some groundbreaking changes within our app for the current
release:
<br />
- JavaFX support <br/>
- Verified groups <br/>
- Group leaving and deletion <br/>
- Group join policy feature update <br/>
- Notification system <br/>
- Stage3 Spotlight <br/>
- Assignment test editing <br/>
- Completion indicator on assignments <br/>
- Some more backlinks <br/>
- Big performance improvements for web <br/>
- Minor bug fixes
- Convert to tutor account <br/>
</Text>
</Card>
</Grid.Col>
Expand Down
20 changes: 20 additions & 0 deletions web/app/groups/[groupId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {isGranted} from "@/service/auth";
import {UserRoles} from "@/service/types/usernator";
import LeaveGroupModal from "@/components/group/LeaveGroupModal";
import DeleteGroupModal from "@/components/group/DeleteGroupModal";
import VerifiedBadge from "@/components/VerifiedBadge";
import NavigateBack from "@/components/NavigateBack";

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

const changeVerifiedState = async () => {
if (group) {
if (group.verified) {
await api.unverify(group.id);
} else {
await api.verify(group.id);
}
refetch();
}
}

useEffect(() => {
if (group) {
addGroup(group);
Expand All @@ -43,12 +56,16 @@ const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {

return (
<Container fluid>
<NavigateBack />
<Group>
<Title>{group?.title ?? "Loading"}</Title>
<Badge>{group?.tutor?.username ?? "Loading"}</Badge>
{group?.join_policy && (
<GroupJoinPolicyBadge policy={group.join_policy} />
)}
{group?.verified && (
<VerifiedBadge />
)}
{(isGranted(user, [UserRoles.Admin]) || group?.tutor.id === user?.id) && (
<>
<Button onClick={() => setUpdateModalOpen(true)}>{t('common:titles.update-group')}</Button>
Expand All @@ -58,6 +75,9 @@ const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
{isGranted(user, [UserRoles.Student]) && (
<Button color="red" onClick={() => setLeaveModalOpen(true)}>{t('group:actions.leave')}</Button>
)}
{isGranted(user, [UserRoles.Admin]) && (
<Button color="cyan" onClick={changeVerifiedState}>{group?.verified ? t('group:actions.unverify') : t('group:actions.verify')}</Button>
)}
</Group>
{group === null ? (
<CentralLoading />
Expand Down
4 changes: 4 additions & 0 deletions web/app/groups/displayComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useCurrentUser from "@/hooks/useCurrentUser";
import { isGranted } from "@/service/auth";
import { useTranslation } from "react-i18next";
import GroupJoinPolicyBadge from "@/components/group/GroupJoinPolicyBadge";
import VerifiedBadge from "@/components/VerifiedBadge";

interface DisplayComponentProps {
groups: MinifiedGroup[];
Expand All @@ -34,6 +35,9 @@ const GroupsDisplayComponent = ({
{
field: "title",
label: t("group:cols.title"),
render: (title, row) => (
<p>{title}&nbsp;{row.verified ? <VerifiedBadge /> : null} </p>
)
},
{
field: "member_count",
Expand Down
Loading
Loading