Skip to content

Commit

Permalink
Merge pull request #164 from MathisBurger/feature/group-notifications
Browse files Browse the repository at this point in the history
Group notifications
  • Loading branch information
MathisBurger authored Nov 20, 2024
2 parents 07a56d9 + dfed46f commit 28ef81f
Show file tree
Hide file tree
Showing 7 changed files with 126 additions and 7 deletions.
3 changes: 2 additions & 1 deletion tasky/src/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,6 @@ pub fn init_services(cfg: &mut web::ServiceConfig) {
.service(code_comment::create_code_comment)
.service(notifications::get_notifiations)
.service(notifications::remove_user_from_notification)
.service(notifications::remove_user_from_all_notifications);
.service(notifications::remove_user_from_all_notifications)
.service(notifications::create_group_notification);
}
45 changes: 43 additions & 2 deletions tasky/src/routes/notifications.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
use actix_web::{delete, get, web, HttpResponse};
use actix_web::{delete, get, post, web, HttpResponse};
use serde::Deserialize;

use crate::{auth_middleware::UserData, models::notification::NotificationRepository, AppState};
use crate::{
auth_middleware::UserData,
error::ApiError,
models::{group::GroupRepository, notification::NotificationRepository},
security::{IsGranted, SecurityAction},
AppState,
};

/// Gets all notifications
#[get("/notifications")]
Expand Down Expand Up @@ -42,3 +49,37 @@ pub async fn remove_user_from_all_notifications(
NotificationRepository::remove_user_from_all_notification(user_data.user_id, db);
HttpResponse::Ok().finish()
}

#[derive(Deserialize)]
struct CreateNotificationRequest {
pub title: String,
pub content: String,
}

/// Endpoint to create group notification
#[post("/groups/{id}/notifications")]
pub async fn create_group_notification(
data: web::Data<AppState>,
user: web::ReqData<UserData>,
body: web::Json<CreateNotificationRequest>,
path: web::Path<(i32,)>,
) -> Result<HttpResponse, ApiError> {
let conn = &mut data.db.db.get().unwrap();

let mut group =
GroupRepository::get_by_id(path.into_inner().0, conn).ok_or(ApiError::BadRequest {
message: "No access to group".to_string(),
})?;
if !group.is_granted(SecurityAction::Update, &user) {
return Err(ApiError::Unauthorized {
message: "Not authorized for action".to_string(),
});
}
NotificationRepository::create_notification_for_group(
body.title.clone(),
body.content.clone(),
group.id,
conn,
);
Ok(HttpResponse::Ok().finish())
}
7 changes: 7 additions & 0 deletions web/app/groups/[groupId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import LeaveGroupModal from "@/components/group/LeaveGroupModal";
import DeleteGroupModal from "@/components/group/DeleteGroupModal";
import VerifiedBadge from "@/components/VerifiedBadge";
import NavigateBack from "@/components/NavigateBack";
import CreateGroupNotificationModal from "@/components/CreateGroupNotificationModal";

const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
const id = parseInt(`${params.groupId}`, 10);
Expand All @@ -27,6 +28,7 @@ const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
const [updateModalOpen, setUpdateModalOpen] = useState<boolean>(false);
const [leaveModalOpen, setLeaveModalOpen] = useState<boolean>(false);
const [deleteModalOpen, setDeleteModalOpen] = useState<boolean>(false);
const [createNotificationModalOpen, setCreateNotificationModalOpen] = useState<boolean>(false);
const { t } = useTranslation("common");

const changeVerifiedState = async () => {
Expand Down Expand Up @@ -70,6 +72,8 @@ const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
<>
<Button onClick={() => setUpdateModalOpen(true)}>{t('common:titles.update-group')}</Button>
<Button color="red" onClick={() => setDeleteModalOpen(true)}>{t('common:actions.delete')}</Button>

<Button color="pink" onClick={() => setCreateNotificationModalOpen(true)}>{t('common:actions.create-notification')}</Button>
</>
)}
{isGranted(user, [UserRoles.Student]) && (
Expand Down Expand Up @@ -97,6 +101,9 @@ const GroupDetailsPage = ({ params }: { params: { groupId: string } }) => {
{deleteModalOpen && group !== null && (
<DeleteGroupModal groupId={group.id} onClose={() => setDeleteModalOpen(false)} />
)}
{createNotificationModalOpen && group !== null && (
<CreateGroupNotificationModal groupId={group.id} onClose={() => setCreateNotificationModalOpen(false)} />
)}
</Container>
);
};
Expand Down
62 changes: 62 additions & 0 deletions web/components/CreateGroupNotificationModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import {Button, Group, Modal, Stack, Textarea, TextInput} from "@mantine/core";
import {useTranslation} from "react-i18next";
import {useForm} from "@mantine/form";
import useApiServiceClient from "@/hooks/useApiServiceClient";
import {showNotification} from "@mantine/notifications";

interface CreateGroupNotificationModalProps {
groupId: number;
onClose: () => void;
}

const CreateGroupNotificationModal = ({groupId, onClose}: CreateGroupNotificationModalProps) => {


const {t} = useTranslation("common");
const api = useApiServiceClient();
const form = useForm({
initialValues: {
title: '',
content: '',
},
validate: {
title: (value) => value.trim() === '' ? t('errors.title-empty') : null,
content: (value) => value.trim() === '' ? t('errors.body-empty') : null
}
});

const submitCallback = form.onSubmit(async (values) => {
try {
await api.createGroupNotification(groupId, values.title, values.content);
showNotification({
title: t('common:messages.success'),
message: t('common:messages.successfully-created-notification')
});
onClose();
} catch (e: any) {
showNotification({
title: t('common:messages.error'),
message: e?.message ?? ''
});
}
});

return (
<Modal opened onClose={onClose} title={t('actions.create-notification')}>
<form onSubmit={submitCallback}>
<Stack gap={3}>
<TextInput label={t('fields.title')} key={form.key('title')} {...form.getInputProps('title')} />
<Textarea label={t('fields.description')} key={form.key('content')} {...form.getInputProps('content')} />
</Stack>
<Group mt={10}>
<Button type="submit">{t("actions.create")}</Button>
<Button onClick={onClose} color="gray">
{t("actions.cancel")}
</Button>
</Group>
</form>
</Modal>
);
}

export default CreateGroupNotificationModal;
6 changes: 4 additions & 2 deletions web/public/locales/de/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"collapse-all": "Alle reduzieren",
"navigate-back": "Zurück navigieren",
"delete": "Löschen",
"clear-all": "Alle löschen"
"clear-all": "Alle löschen",
"create-notification": "Benachrichtigung erstellen"
},
"messages": {
"login-failed": "Anmeldung fehlgeschlagen",
Expand All @@ -63,7 +64,8 @@
"no-files-selected": "Keine Dateien ausgewählt",
"copied-code": "Code kopiert!",
"rejected-files": "Abgelehnte Dateien",
"switch-to-tutor-text": "Bist du sicher, dass du zu einem Tutor-Konto wechseln möchtest? Das bedeutet, dass du keine Aufgaben mehr bearbeiten kannst. Dsfür benötigst du einen weiteren Account. Also Tutor kannst du nur Aufgaben erstellen und verwalten innerhalb deiner eigenen Gruppen. Sei dir also deiner Entscheidung bewusst"
"switch-to-tutor-text": "Bist du sicher, dass du zu einem Tutor-Konto wechseln möchtest? Das bedeutet, dass du keine Aufgaben mehr bearbeiten kannst. Dsfür benötigst du einen weiteren Account. Also Tutor kannst du nur Aufgaben erstellen und verwalten innerhalb deiner eigenen Gruppen. Sei dir also deiner Entscheidung bewusst",
"successfully-created-notification": "Benachrichtigung erfolgreich erstelt"
},
"errors": {
"title-empty": "Der Titel darf nicht leer sein",
Expand Down
6 changes: 4 additions & 2 deletions web/public/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
"collapse-all": "Collapse all",
"navigate-back": "Navigate back",
"delete": "Delete",
"clear-all": "Clear all"
"clear-all": "Clear all",
"create-notification": "Create notification"
},
"messages": {
"login-failed": "Login failed",
Expand All @@ -63,7 +64,8 @@
"no-files-selected": "No files selected",
"copied-code": "Copied code!",
"rejected-files": "Rejected files",
"switch-to-tutor-text": "Are you sure you want to switch to a tutor account? This means you will no longer be able to work on tasks. For that, you would need a separate account. As a tutor, you can only create and manage tasks within your own groups. Please be sure of your decision."
"switch-to-tutor-text": "Are you sure you want to switch to a tutor account? This means you will no longer be able to work on tasks. For that, you would need a separate account. As a tutor, you can only create and manage tasks within your own groups. Please be sure of your decision.",
"successfully-created-notification": "Successfully created notification"
},
"errors": {
"title-empty": "The title should not be empty",
Expand Down
4 changes: 4 additions & 0 deletions web/service/ApiService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,10 @@ class ApiService {
return await this.get<AssignmentCompletionsResponse>(`/tasky/groups/${groupId}/assignments/${assignmentId}/completions?page=${page}`);
}

public async createGroupNotification(groupId: number, title: string, content: string): Promise<void> {
await this.post<any>(`/tasky/groups/${groupId}/notifications`, {title, content})
}

public async createOrUpdateCodeTests(
groupId: number,
assignmentId: number,
Expand Down

0 comments on commit 28ef81f

Please sign in to comment.