Skip to content

Commit

Permalink
Merge pull request #165 from MathisBurger/feature/system-wide-notific…
Browse files Browse the repository at this point in the history
…ations

System wide notifications
  • Loading branch information
MathisBurger authored Nov 20, 2024
2 parents 28ef81f + c1aef1a commit 1341356
Show file tree
Hide file tree
Showing 20 changed files with 326 additions and 34 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-- This file should undo anything in `up.sql`
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE notifications ADD COLUMN show_until TIMESTAMP;
ALTER TABLE notifications ADD COLUMN system_wide BOOLEAN NOT NULL DEFAULT false;
7 changes: 6 additions & 1 deletion tasky/src/deletion_scheduler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ pub async fn scheduler(conn: &mut DB) {
)
.execute(conn)
.expect("Cannot delete notifications");

diesel::delete(
notification_dsl::notifications
.filter(notification_dsl::show_until.lt(diesel::dsl::now)),
)
.execute(conn)
.expect("Cannot delete system wide notifications");
interval.tick().await;
}
}
44 changes: 44 additions & 0 deletions tasky/src/models/notification.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::schema::group_members;
use crate::schema::notification_targets;
use crate::schema::notifications::dsl;
use crate::schema::notifications::table;
use diesel::associations::HasTable;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
Expand All @@ -21,6 +22,8 @@ pub struct Notification {
pub content: String,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub show_until: Option<NaiveDateTime>,
pub system_wide: bool,
}

/// notification insert type for external calls
Expand All @@ -36,6 +39,8 @@ pub struct CreateNotification {
struct InternalCreate {
pub title: String,
pub content: String,
pub show_until: Option<NaiveDateTime>,
pub system_wide: bool,
}

pub struct NotificationRepository;
Expand All @@ -46,6 +51,8 @@ impl NotificationRepository {
let notification_create = InternalCreate {
title: notification.title.clone(),
content: notification.content.clone(),
show_until: None,
system_wide: false,
};

let created = diesel::insert_into(dsl::notifications::table())
Expand Down Expand Up @@ -121,4 +128,41 @@ impl NotificationRepository {
.execute(conn)
.expect("Cannot remove user from notification");
}

/// Creates an system wide notification
pub fn create_system_wide_notification(
title: String,
content: String,
show_until: NaiveDateTime,
conn: &mut DB,
) {
diesel::insert_into(table)
.values(InternalCreate {
title: title.clone(),
content: content.clone(),
show_until: Some(show_until),
system_wide: true,
})
.execute(conn)
.expect("Cannot create system wide notification");
}

/// Gets all system wide notificytions
pub fn get_system_wide(conn: &mut DB) -> Vec<Notification> {
dsl::notifications
.filter(
dsl::system_wide
.eq(true)
.and(dsl::show_until.gt(diesel::dsl::now)),
)
.get_results::<Notification>(conn)
.expect("Cannot load all system wide notifications")
}

/// Deletes an notification by ID
pub fn delete(id: i32, conn: &mut DB) {
diesel::delete(dsl::notifications.filter(dsl::id.eq(id)))
.execute(conn)
.expect("Cannot delete notification");
}
}
25 changes: 3 additions & 22 deletions tasky/src/routes/assignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,34 +28,15 @@ use actix_web::get;
use actix_web::post;
use actix_web::web;
use actix_web::HttpResponse;
use chrono::DateTime;
use chrono::NaiveDateTime;
use serde::Deserialize;
use serde::Serialize;
use serde::{Deserialize, Deserializer};

fn deserialize_naive_datetime<'de, D>(deserializer: D) -> Result<Option<NaiveDateTime>, D::Error>
where
D: Deserializer<'de>,
{
let str_option: Option<String> = Deserialize::deserialize(deserializer).ok();
if str_option.is_none() {
return Ok(None);
}
let s = str_option.unwrap();
if let Ok(datetime_with_tz) = DateTime::parse_from_rfc3339(s.as_str()) {
// Convert to NaiveDateTime by discarding the time zone
return Ok(Some(datetime_with_tz.naive_utc()));
}
NaiveDateTime::parse_from_str(s.as_str(), "%Y-%m-%dT%H:%M:%S")
.map_err(serde::de::Error::custom)
.map(Some)
}

/// Request to create an assignment
#[derive(Deserialize, Serialize)]
pub struct CreateAssignmentRequest {
pub title: String,
#[serde(deserialize_with = "deserialize_naive_datetime")]
#[serde(deserialize_with = "crate::routes::deserialize_naive_datetime")]
pub due_date: Option<NaiveDateTime>,
pub description: String,
pub language: AssignmentLanguage,
Expand All @@ -65,7 +46,7 @@ pub struct CreateAssignmentRequest {
#[derive(Deserialize, Serialize)]
pub struct UpdateAssignmentRequest {
pub title: String,
#[serde(deserialize_with = "deserialize_naive_datetime")]
#[serde(deserialize_with = "crate::routes::deserialize_naive_datetime")]
pub due_date: Option<NaiveDateTime>,
pub description: String,
}
Expand Down
28 changes: 26 additions & 2 deletions tasky/src/routes/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use actix_web::web;
use serde::Deserialize;
use chrono::{DateTime, NaiveDateTime};
use serde::{Deserialize, Deserializer};

pub mod assignment;
pub mod assignment_completion;
Expand Down Expand Up @@ -70,5 +71,28 @@ pub fn init_services(cfg: &mut web::ServiceConfig) {
.service(notifications::get_notifiations)
.service(notifications::remove_user_from_notification)
.service(notifications::remove_user_from_all_notifications)
.service(notifications::create_group_notification);
.service(notifications::create_group_notification)
.service(notifications::create_system_wide_notifications)
.service(notifications::delete_system_wide_notifications)
.service(notifications::get_system_wide_notifications);
}

pub fn deserialize_naive_datetime<'de, D>(
deserializer: D,
) -> Result<Option<NaiveDateTime>, D::Error>
where
D: Deserializer<'de>,
{
let str_option: Option<String> = Deserialize::deserialize(deserializer).ok();
if str_option.is_none() {
return Ok(None);
}
let s = str_option.unwrap();
if let Ok(datetime_with_tz) = DateTime::parse_from_rfc3339(s.as_str()) {
// Convert to NaiveDateTime by discarding the time zone
return Ok(Some(datetime_with_tz.naive_utc()));
}
NaiveDateTime::parse_from_str(s.as_str(), "%Y-%m-%dT%H:%M:%S")
.map_err(serde::de::Error::custom)
.map(Some)
}
64 changes: 63 additions & 1 deletion tasky/src/routes/notifications.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use actix_web::{delete, get, post, web, HttpResponse};
use chrono::NaiveDateTime;
use serde::Deserialize;

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

Expand Down Expand Up @@ -54,6 +55,48 @@ pub async fn remove_user_from_all_notifications(
struct CreateNotificationRequest {
pub title: String,
pub content: String,
#[serde(deserialize_with = "crate::routes::deserialize_naive_datetime")]
pub show_until: Option<NaiveDateTime>,
}

/// Endpoint to create system wide notification
#[post("/system_wide_notifications")]
pub async fn create_system_wide_notifications(
data: web::Data<AppState>,
user: web::ReqData<UserData>,
body: web::Json<CreateNotificationRequest>,
) -> Result<HttpResponse, ApiError> {
let conn = &mut data.db.db.get().unwrap();

if !StaticSecurity::is_granted(StaticSecurityAction::IsAdmin, &user) {
return Err(ApiError::Forbidden {
message: "You cannot create system wide notificytions".to_string(),
});
}
if body.show_until.is_none() {
return Err(ApiError::BadRequest {
message: "Please supply an active deadline".to_string(),
});
}
NotificationRepository::create_system_wide_notification(
body.title.clone(),
body.content.clone(),
body.show_until.unwrap(),
conn,
);
Ok(HttpResponse::Ok().finish())
}

/// Endpoint to get system wide notifications
#[get("/system_wide_notifications")]
pub async fn get_system_wide_notifications(
data: web::Data<AppState>,
_: web::ReqData<UserData>,
) -> Result<HttpResponse, ApiError> {
let conn = &mut data.db.db.get().unwrap();

let notifications = NotificationRepository::get_system_wide(conn);
Ok(HttpResponse::Ok().json(notifications))
}

/// Endpoint to create group notification
Expand Down Expand Up @@ -83,3 +126,22 @@ pub async fn create_group_notification(
);
Ok(HttpResponse::Ok().finish())
}

/// Endpoint to delete system wide notification
#[delete("/system_wide_notifications/{id}")]
pub async fn delete_system_wide_notifications(
data: web::Data<AppState>,
user: web::ReqData<UserData>,
path: web::Path<(i32,)>,
) -> Result<HttpResponse, ApiError> {
let conn = &mut data.db.db.get().unwrap();

if !StaticSecurity::is_granted(StaticSecurityAction::IsAdmin, &user) {
return Err(ApiError::BadRequest {
message: "You are not allowed to delete".to_string(),
});
}

NotificationRepository::delete(path.into_inner().0, conn);
Ok(HttpResponse::Ok().finish())
}
2 changes: 2 additions & 0 deletions tasky/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ diesel::table! {
content -> Text,
created_at -> Timestamp,
updated_at -> Timestamp,
show_until -> Nullable<Timestamp>,
system_wide -> Bool,
}
}

Expand Down
22 changes: 21 additions & 1 deletion web/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,38 @@
"use client";
import useCurrentUser from "@/hooks/useCurrentUser";
import { Container, Title, Text, Card, Grid, Group, Flex } from "@mantine/core";
import {Container, Title, Text, Card, Grid, Group, Flex, Box} from "@mantine/core";
import { IconTrophyFilled } from "@tabler/icons-react";
import { useTranslation } from "react-i18next";
import useApiServiceClient from "@/hooks/useApiServiceClient";
import useClientQuery from "@/hooks/useClientQuery";
import {Carousel} from "@mantine/carousel";
import RichTextDisplay from "@/components/display/RichTextDisplay";

const DashboardPage = () => {
const { user } = useCurrentUser();
const { t } = useTranslation("dashboard");
const api = useApiServiceClient();
const [notifications] = useClientQuery(() => api.getSystemWideNotification());

return (
<Container fluid>
<Title>
{t("welcome-back")} {user?.username}!
</Title>
{notifications && notifications?.length > 0 && (
<Carousel withIndicators height={200}>
{notifications.map((notification) => (
<Carousel.Slide key={notification.id}>
<Card h={200}>
<Box mx="xl">
<Title order={2}>{notification.title}</Title>
<RichTextDisplay content={notification.content} fullSize={false} />
</Box>
</Card>
</Carousel.Slide>
))}
</Carousel>
)}
<Card shadow="sm" padding="xl" mt={20}>
<Text mt="xs" c="dimmed" size="sm">
{t("us-again-text")}
Expand Down
1 change: 1 addition & 0 deletions web/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import "@mantine/notifications/styles.css";
import "@mantine/spotlight/styles.css";
import "@mantine/code-highlight/styles.css";
import "@mantine/dropzone/styles.css";
import '@mantine/carousel/styles.css';
import { DatesProvider } from "@mantine/dates";
import SpotlightWrapper from "@/components/spotlight/SpotlightWrapper";
import Footer from "@/components/Footer";
Expand Down
56 changes: 56 additions & 0 deletions web/app/notifications/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
"use client";
import {Button, Container, Stack, Title} from "@mantine/core";
import {useTranslation} from "react-i18next";
import EntityList, {EntityListCol, EntityListRowAction} from "@/components/EntityList";
import RichTextDisplay from "@/components/display/RichTextDisplay";
import useClientQuery from "@/hooks/useClientQuery";
import useApiServiceClient from "@/hooks/useApiServiceClient";
import {useState} from "react";
import CreateSystemWideNotificationModal from "@/components/CreateSystemWideNotificationModal";


const NotificationsPage = () => {

const {t} = useTranslation('common');
const api = useApiServiceClient();
const [notifications, refetch] = useClientQuery(() => api.getSystemWideNotification());
const [createModalOpen, setCreateModalOpen] = useState<boolean>(false);

const cols: EntityListCol[] = [
{
field: 'title',
label: t('common:fields.title')
},
{
field: 'content',
label: t('common:fields.description'),
render: (value) => <RichTextDisplay content={value as string} fullSize={false} />
}
];

const rowActions: EntityListRowAction[] = [
{
name: t('actions.delete'),
onClick: async (row) => {
await api.deleteSystemWideNotifications(row.id);
refetch();
},
color: 'red'
}
]

return (
<Container fluid>
<Stack gap={5}>
<Title>{t('common:titles.system-wide-notifications')}</Title>
<Button color="indigo" onClick={() => setCreateModalOpen(true)}>{t('common:actions.create-notification')}</Button>
<EntityList cols={cols} rows={notifications ?? []} rowActions={rowActions} />
</Stack>
{createModalOpen && (
<CreateSystemWideNotificationModal onClose={() => setCreateModalOpen(false)} refetch={refetch} />
)}
</Container>
);
}

export default NotificationsPage;
Binary file modified web/bun.lockb
Binary file not shown.
Loading

0 comments on commit 1341356

Please sign in to comment.