-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #91 from MathisBurger/feature/code-comments
Code Comments Feature
- Loading branch information
Showing
14 changed files
with
320 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
DROP TABLE code_comments; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
CREATE TABLE code_comments ( | ||
id SERIAL PRIMARY KEY, | ||
title VARCHAR(255) NOT NULL, | ||
content TEXT NOT NULL, | ||
commentor INTEGER NOT NULL, | ||
group_id INTEGER NOT NULL REFERENCES groups(id), | ||
solution_id INTEGER NOT NULL REFERENCES solutions(id) | ||
); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
use super::DB; | ||
use crate::schema::code_comments::dsl; | ||
use diesel::associations::HasTable; | ||
use diesel::prelude::*; | ||
use diesel::Selectable; | ||
use serde::Serialize; | ||
|
||
/// code comment entity type | ||
#[derive(Queryable, Selectable, Clone, Serialize)] | ||
#[diesel(table_name = crate::schema::code_comments)] | ||
#[diesel(check_for_backend(diesel::pg::Pg))] | ||
pub struct CodeComment { | ||
pub id: i32, | ||
pub title: String, | ||
pub content: String, | ||
pub commentor: i32, | ||
pub group_id: i32, | ||
pub solution_id: i32, | ||
} | ||
|
||
/// Create comment struct to create a code comment | ||
#[derive(Insertable)] | ||
#[diesel(table_name = crate::schema::code_comments)] | ||
pub struct CreateCodeComment { | ||
pub title: String, | ||
pub content: String, | ||
pub commentor: i32, | ||
pub group_id: i32, | ||
pub solution_id: i32, | ||
} | ||
|
||
pub struct CodeCommentRepository; | ||
|
||
impl CodeCommentRepository { | ||
/// Creates a new code comment | ||
pub fn create_comment(create: &CreateCodeComment, conn: &mut DB) -> CodeComment { | ||
diesel::insert_into(dsl::code_comments::table()) | ||
.values(create) | ||
.returning(CodeComment::as_returning()) | ||
.get_result::<CodeComment>(conn) | ||
.expect("Cannot create new code comment") | ||
} | ||
|
||
/// Gets all comments for solution | ||
pub fn get_comments_for_solution(solution_id: i32, conn: &mut DB) -> Vec<CodeComment> { | ||
dsl::code_comments | ||
.filter(dsl::solution_id.eq(solution_id)) | ||
.get_results::<CodeComment>(conn) | ||
.expect("Cannot fetch comments") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,84 @@ | ||
use crate::auth_middleware::UserData; | ||
use crate::models::solution::SolutionRepository; | ||
use crate::models::DB; | ||
use crate::security::SecurityAction; | ||
use crate::AppState; | ||
use crate::{ | ||
error::ApiError, | ||
models::{ | ||
code_comment::{CodeCommentRepository, CreateCodeComment}, | ||
solution::Solution, | ||
}, | ||
security::IsGranted, | ||
}; | ||
use actix_web::{get, post, web, HttpResponse}; | ||
use serde::Deserialize; | ||
|
||
#[derive(Deserialize)] | ||
pub struct CodeCommentRequest { | ||
pub title: String, | ||
pub content: String, | ||
} | ||
|
||
/// Gets all code comments for a solution | ||
#[get("/solutions/{solution_id}/code_comments")] | ||
pub async fn get_code_comments( | ||
data: web::Data<AppState>, | ||
user: web::ReqData<UserData>, | ||
path: web::Path<(i32,)>, | ||
) -> Result<HttpResponse, ApiError> { | ||
let user_data = user.into_inner(); | ||
let path_data = path.into_inner(); | ||
let conn = &mut data.db.db.get().unwrap(); | ||
|
||
let solution = get_solution(path_data.0, &user_data, conn)?; | ||
let comments = CodeCommentRepository::get_comments_for_solution(solution.id, conn); | ||
Ok(HttpResponse::Ok().json(comments)) | ||
} | ||
|
||
/// Creates a code comment on a solution | ||
#[post("/solutions/{solution_id}/code_comments")] | ||
pub async fn create_code_comment( | ||
data: web::Data<AppState>, | ||
user: web::ReqData<UserData>, | ||
req: web::Json<CodeCommentRequest>, | ||
path: web::Path<(i32,)>, | ||
) -> Result<HttpResponse, ApiError> { | ||
let user_data = user.into_inner(); | ||
let path_data = path.into_inner(); | ||
let conn = &mut data.db.db.get().unwrap(); | ||
|
||
let solution = get_solution(path_data.0, &user_data, conn)?; | ||
let mut create = CreateCodeComment { | ||
title: req.title.clone(), | ||
content: req.content.clone(), | ||
commentor: user_data.user_id, | ||
group_id: solution.group_id.unwrap_or(-1), | ||
solution_id: solution.id, | ||
}; | ||
if !create.is_granted(SecurityAction::Create, &user_data) { | ||
return Err(ApiError::Forbidden { | ||
message: "You are not allowed to create a code comment".to_string(), | ||
}); | ||
} | ||
let comment = CodeCommentRepository::create_comment(&create, conn); | ||
Ok(HttpResponse::Ok().json(comment)) | ||
} | ||
|
||
/// Gets solution and checks basic read permissions | ||
fn get_solution( | ||
solution_id: i32, | ||
user_data: &UserData, | ||
conn: &mut DB, | ||
) -> Result<Solution, ApiError> { | ||
let mut solution = | ||
SolutionRepository::get_solution_by_id(solution_id, conn).ok_or(ApiError::BadRequest { | ||
message: "Invalid solution".to_string(), | ||
})?; | ||
if !solution.is_granted(SecurityAction::Read, user_data) { | ||
return Err(ApiError::Forbidden { | ||
message: "You have no access to solution".to_string(), | ||
}); | ||
} | ||
Ok(solution) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
use crate::models::code_comment::CreateCodeComment; | ||
|
||
use super::{IsGranted, SecurityAction, StaticSecurity}; | ||
|
||
impl IsGranted for CreateCodeComment { | ||
fn is_granted( | ||
&mut self, | ||
action: super::SecurityAction, | ||
user: &crate::auth_middleware::UserData, | ||
) -> bool { | ||
if action == SecurityAction::Create { | ||
return StaticSecurity::is_granted(super::StaticSecurityAction::IsAdmin, user) | ||
|| (StaticSecurity::is_granted(super::StaticSecurityAction::IsTutor, user) | ||
&& user.groups.contains(&self.group_id)); | ||
} | ||
false | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import useApiServiceClient from "@/hooks/useApiServiceClient"; | ||
import useClientQuery from "@/hooks/useClientQuery"; | ||
import { Solution } from "@/service/types/tasky"; | ||
import {Badge, Button, Card, Group, Stack, Title} from "@mantine/core"; | ||
import AssignmentDateDisplay from "@/components/assignments/AssignmentDateDisplay"; | ||
import RichTextDisplay from "@/components/display/RichTextDisplay"; | ||
import useCurrentUser from "@/hooks/useCurrentUser"; | ||
import {IconPlus} from "@tabler/icons-react"; | ||
import {useState} from "react"; | ||
import CreateCommentModal from "@/components/solution/CreateCommentModal"; | ||
|
||
interface CommentTabProps { | ||
solution: Solution; | ||
} | ||
|
||
const CommentTab = ({solution}: CommentTabProps) => { | ||
|
||
const api = useApiServiceClient(); | ||
const {user} = useCurrentUser(); | ||
const [createModalOpen, setCreateModalOpen] = useState(false); | ||
const [comments, refetch] = useClientQuery(() => api.getCodeComments(solution.id)); | ||
|
||
return ( | ||
<> | ||
<Stack gap={10}> | ||
<Group justify="flex-end"> | ||
<Button onClick={() => setCreateModalOpen(true)}><IconPlus /> | ||
Create Comment</Button> | ||
</Group> | ||
{(comments ?? []).map((comment) => ( | ||
<Card shadow="sm" padding="lg" radius="md" withBorder key={comment.id}> | ||
<Group> | ||
<Title order={4}>{comment.title}</Title> | ||
{comment.commentor === user?.id && ( | ||
<Badge color="green">Your comment</Badge> | ||
)} | ||
</Group> | ||
<RichTextDisplay content={comment.content} fullSize={false} /> | ||
</Card> | ||
))} | ||
</Stack> | ||
{createModalOpen && ( | ||
<CreateCommentModal | ||
solution={solution} | ||
refetch={refetch} | ||
onClose={() => setCreateModalOpen(false)} | ||
/> | ||
)} | ||
</> | ||
); | ||
} | ||
|
||
export default CommentTab; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
import { Solution } from "@/service/types/tasky"; | ||
import {Button, Divider, Group, Modal, Stack, TextInput} from "@mantine/core"; | ||
import {useForm} from "@mantine/form"; | ||
import RichTextInput from "@/components/form/RichTextInput"; | ||
import useApiServiceClient from "@/hooks/useApiServiceClient"; | ||
import {notifications} from "@mantine/notifications"; | ||
|
||
|
||
interface CreateCommentModalProps { | ||
solution: Solution; | ||
refetch: () => void; | ||
onClose: () => void; | ||
} | ||
|
||
const CreateCommentModal = ({solution, refetch, onClose}: CreateCommentModalProps) => { | ||
|
||
const form = useForm({ | ||
initialValues: { | ||
title: '', | ||
content: '' | ||
} | ||
}); | ||
const api = useApiServiceClient(); | ||
|
||
const onSubmit = form.onSubmit(async (values) => { | ||
try { | ||
await api.createCodeComment(solution.id, values.title, values.content); | ||
refetch(); | ||
onClose(); | ||
} catch (e: any) { | ||
notifications.show({ | ||
title: 'Error', | ||
message: e?.message ?? "Failed to create comment", | ||
}) | ||
} | ||
}); | ||
|
||
return ( | ||
<Modal opened onClose={onClose} title="Create code comment" size="xl"> | ||
<form onSubmit={onSubmit}> | ||
<Stack gap={10}> | ||
<TextInput label="Title" key={form.key('title')} {...form.getInputProps('title')} /> | ||
<RichTextInput | ||
key={form.key('content')} | ||
content={form.getInputProps('content').value} | ||
setContent={form.getInputProps('content').onChange} | ||
/> | ||
</Stack> | ||
<Divider mt={10} /> | ||
<Group mt={10}> | ||
<Button type="submit"> | ||
Create questions | ||
</Button> | ||
<Button onClick={onClose} color="gray"> | ||
Cancel | ||
</Button> | ||
</Group> | ||
</form> | ||
</Modal> | ||
); | ||
} | ||
|
||
export default CreateCommentModal; |
Oops, something went wrong.