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

Forum Web Components #464

Closed
wants to merge 39 commits into from
Closed
Show file tree
Hide file tree
Changes from 32 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
75eda0e
ADD docker column with on/off
May 22, 2023
49747dc
FIX slight linting error
May 22, 2023
b6ae400
CHG shifted docker column to left
May 22, 2023
15a8cd6
ADD Functions for regrading by student
dolf321 May 22, 2023
18024d4
ADD added grading subs by deadline
dolf321 May 22, 2023
d3ce7ee
ADD Enqueue for new function
dolf321 May 22, 2023
22342a2
CHG Increased limit of stud regrade
dolf321 May 22, 2023
9570ad0
ADD Route for student regrade
dolf321 May 22, 2023
723788f
CHG Increased limit of stud regrade
dolf321 May 22, 2023
7f80d88
ADD button,dialogue, and text MUI components
dolf321 May 22, 2023
750acc9
FIX netid param name
dolf321 May 22, 2023
427c3cd
ADD assignment submission & autofill
dolf321 May 22, 2023
5a1f900
ADD status for student regrade
dolf321 May 22, 2023
42c6dd2
ADD pytest-timeout to requirements for testing
dolf321 May 22, 2023
d98b974
ADD regrading tests
dolf321 May 22, 2023
35ca641
ADD Writing a post functionality
dolf321 Jul 16, 2023
b7cb9a3
ADD image embed functionality
dolf321 Jul 16, 2023
e0a1e58
ADD Writing a post functionality
dolf321 Jul 16, 2023
81ca3f7
ADD image embed functionality
dolf321 Jul 16, 2023
fac1d9c
Merge branch 'main' of https://github.com/dolf321/Anubis
dolf321 Jul 17, 2023
6af0419
CHG migrate text editor to separate component
dolf321 Jul 19, 2023
cb2a3ad
CHG migrate text editor to new folder
dolf321 Jul 20, 2023
fb6b531
ADD commenting functionality
dolf321 Jul 20, 2023
69542d7
CHG cleaned up post dialog
dolf321 Jul 20, 2023
c2ad04d
ADD commenting with richtexteditor
dolf321 Jul 20, 2023
b3d47f6
ADD content summaries to post list items
dolf321 Jul 20, 2023
a13889f
ADD cleaned up rich text editor
dolf321 Jul 20, 2023
4fc636d
ADD Forum with temp mock data
dolf321 Jul 20, 2023
ae75c36
FIX console errors
dolf321 Jul 21, 2023
fa95147
CHG fixed post info size
dolf321 Jul 21, 2023
5fae04f
ADD Some basic documentation to the RichTextEditor
dolf321 Jul 21, 2023
15e96cc
CHG cleaned up imports
dolf321 Jul 21, 2023
dce8026
FIX promise related issue with image upload
dolf321 Jul 21, 2023
356a195
ADD refreshing selected post
dolf321 Jul 21, 2023
af37842
CHG Condition to lambda operator
dolf321 Jul 22, 2023
3a0c0e2
FIX slight bug in comment component
dolf321 Jul 22, 2023
62dacf7
CHG cleaned up forum refresh logic
dolf321 Jul 22, 2023
0233fe8
CHG removed unecesary arrow function
dolf321 Jul 22, 2023
77094ab
FIX useless parameter
dolf321 Jul 22, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions api/anubis/lms/regrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,52 @@ def bulk_regrade_assignment_of_student(

chunk_and_enqueue_regrade(submissions)

@with_context
def bulk_regrade_assignment_of_student(
user_id: str,
hours: int = -1,
not_processed: int = -1,
processed: int = -1,
reaped: int = -1,
latest_only: int = -1,
):

# Build a list of filters based on the options
filters = []
if latest_only <= 1:

# Number of hours back to regrade
if hours > 0:
filters.append(Submission.created > datetime.now() - timedelta(hours=hours))

# Only regrade submissions that have been processed
if processed == 1:
filters.append(Submission.processed == True)

# Only regrade submissions that have not been processed
if not_processed == 1:
filters.append(Submission.processed == False)

# Only regrade submissions that have been reaped
if reaped == 1:
filters.append(Submission.state == "Reaped after timeout")

from anubis.lms.submissions import get_latest_user_submissions
submissions = get_latest_user_submissions(user=user_id, limit=100, filter=filters)

# Proceed to only filter submissions that are not past the assignment due date (including grace date)
from anubis.lms.assignments import get_assignment_due_date
submissions = [s for s in submissions if s.created < get_assignment_due_date(user_id, s.assignment_id, True)]

# Split the submissions into bite sized chunks
submission_ids = [s.id for s in submissions]
submission_chunks = split_chunks(submission_ids, 100)

from anubis.rpc.enqueue import enqueue_bulk_regrade_submissions
# Enqueue each chunk as a job for the rpc workers
for chunk in submission_chunks:
enqueue_bulk_regrade_submissions(chunk)


@with_context
def bulk_regrade_assignment(
Expand Down
45 changes: 45 additions & 0 deletions api/anubis/views/admin/regrade.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,51 @@ def admin_regrade_status(assignment: Assignment):
}
)

#
@regrade.route("/status/student/<string:netid>")
@require_admin()
@json_response
def admin_regrade_status_student(netid: str):
"""
Get the autograde status for a student. The status
is some high level stats the proportion of submissions
within the assignments for that student that have been processed

:param netid:
:return:
"""
# Get the student
student: User = User.query.filter(User.netid == netid).first()

# Verify the student exists
req_assert(student is not None, message="student does not exist")

# Assert that the course exists
assert_course_context(student)

# Get the number of submissions that are being processed
processing = Submission.query.filter(
Submission.owner_id == student.id,
Submission.processed == False
).count()

# Get the total number of submissions
total = Submission.query.filter(
Submission.owner_id == student.id,
).count()

# Calculate the percent of submissions that have been processed
percent = math.ceil(((total - processing) / total) * 100) if total > 0 else 0

# Return the status
return success_response(
{
"percent": f"{percent}% of submissions processed",
"processing": processing,
"processed": total - processing,
"total": total,
}
)

@regrade.route("/status/student/<string:id>")
@require_admin()
Expand Down
4 changes: 1 addition & 3 deletions api/tests/test_regrade_admin.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import pytest

import pytest, time
from anubis.models import Submission
from utils import Session, permission_test, with_context


@with_context
def get_student_submission_commit(assignment_ids):
for assignment_id in assignment_ids:
Expand All @@ -14,7 +14,6 @@ def get_student_submission_commit(assignment_ids):
if submission is not None:
return submission.commit, assignment_id


def test_regrade_admin():
superuser = Session("superuser")
assignments = superuser.get("/admin/assignments/list")["assignments"]
Expand All @@ -25,7 +24,6 @@ def test_regrade_admin():
permission_test(f"/admin/regrade/submission/{commit}")
permission_test(f"/admin/regrade/assignment/{assignment_id}")


@pytest.mark.timeout(40)
def test_regrade_admin_student():
superuser = Session("superuser")
Expand Down
4 changes: 4 additions & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
"dependencies": {
"@date-io/core": "^1.3.6",
"@date-io/date-fns": "^1.3.13",
"@draft-js-plugins/editor": "^4.1.3",
"@draft-js-plugins/image": "^4.1.3",
"@draft-js-plugins/resizeable": "^5.0.3",
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@mui/icons-material": "^5.8.4",
Expand All @@ -31,6 +34,7 @@
"react-device-detect": "^2.2.2",
"react-diff-view": "^2.4.10",
"react-dom": "^18.2.0",
"react-draft-wysiwyg": "^1.15.0",
"react-markdown": "^5.0.2",
"react-router-dom": "^5.1.2",
"react-scripts": "^5.0.1",
Expand Down
6 changes: 2 additions & 4 deletions web/src/components/forums/Comment/Comment.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import React, {useState} from 'react';
import axios from 'axios';
import {useSnackbar} from 'notistack';

import Box from '@mui/material/Box';
Expand All @@ -9,9 +8,8 @@ import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';

import {useStyles} from './Comment.styles';
import standardErrorHandler from '../../../utils/standardErrorHandler';
import standardStatusHandler from '../../../utils/standardStatusHandler';
import {toRelativeDate} from '../../../utils/datetime';
import RichTextEditor from '../Editor/RichTextEditor';

export default function Comment({
threadStart = false,
Expand Down Expand Up @@ -42,7 +40,7 @@ export default function Comment({
</Typography>
</Box>
<Typography className={classes.content}>
{content}
<RichTextEditor content={content} readOnly={true} enableToolbar={false}/>
</Typography>
<Box className={classes.replyActions}>
{hasReplies &&
Expand Down
7 changes: 4 additions & 3 deletions web/src/components/forums/CommentsList/CommentsList.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import React, {useState} from 'react';

import Box from '@mui/material/Box';
import Comment from '../Comment/Comment';

import Publisher from '../Publisher/Publisher';
import {useStyles} from './CommentsList.styles';

export default function CommentsList({comments}) {
export default function CommentsList({comments, handleCreateComment}) {
const classes = useStyles();

const [isReplying, setIsReplying] = useState(false);
Expand Down Expand Up @@ -39,7 +39,8 @@ export default function CommentsList({comments}) {
/>
))}
{isReplying &&
<input />
<Publisher mode="comment" setOpen={setIsReplying}
handlePublish={(comment) => handleCreateComment({...comment, comment_id: comment.id})}/>
}
</Box>
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export const useStyles = makeStyles((theme) => ({
flexDirection: 'column',
borderLeft: `1px solid ${theme.palette.dark.blue['200']}`,
paddingLeft: theme.spacing(1.5),
overflow: 'scroll',
},
replies: {
display: 'flex',
Expand Down
58 changes: 14 additions & 44 deletions web/src/components/forums/CreateDialog/CreateDialog.jsx
Original file line number Diff line number Diff line change
@@ -1,61 +1,31 @@
import React, {useState} from 'react';

import Dialog from '@mui/material/Dialog';
import Box from '@mui/material/Box';
import Input from '@mui/material/Input';
import TextField from '@mui/material/TextField';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Switch from '@mui/material/Switch';

import {Editor, EditorState} from 'draft-js';

import {useStyles} from './CreateDialog.styles';
import {Dialog} from '@mui/material';
import makeStyles from '@mui/styles/makeStyles';
import Publisher from '../Publisher/Publisher';
export const useStyles = makeStyles((theme) => ({
root: {
width: '800px',
},
}));

export default function CreateDialog({
mode = 'post',
isOpen = false,
open = false,
setOpen,
handleCreatePost,
}) {
// MUI theme-based css styles
const classes = useStyles();
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [isVisibleToStudents, setIsVisisbleToStudents] = useState(true);
const [isAnonymous, setIsAnonymous] = useState(false);
const [error, setError] = useState('');

const validatePost = () => {
if (title && content) {
handleCreatePost({
title: title,
content: content,
visible_to_students: isVisibleToStudents,
anonymous: isAnonymous,
});
};
};

return (
<Dialog
isFullScreen
open={isOpen}
open={open}
classes={{paper: classes.root}}
onClose={() => setOpen(false)}
>
{error}
<Typography>
{mode === 'post' ? 'Create a new post' : 'Create a new comment'}
</Typography>
<Input value={title} onChange={(e) => setTitle(e.target.value)} placeholder={'Post Title'} />
<TextField value={content} onChange={(e) => setContent(e.target.value)}/>
<div className={classes.switchContainer}>
<p> Visibile to Students ? </p>
<Switch checked={isVisibleToStudents} onChange={() => setIsVisisbleToStudents(!isVisibleToStudents)}/>
</div>
<div className={classes.switchContainer}>
<p>Anonymous ? </p>
<Switch checked={isAnonymous} onChange={() => setIsAnonymous(!isAnonymous)}/>
</div>
<Button onClick={validatePost}> Post </Button>
<Publisher mode={mode} setOpen={setOpen} handlePublish={handleCreatePost}/>
</Dialog>
);
}
Expand Down
12 changes: 0 additions & 12 deletions web/src/components/forums/CreateDialog/CreateDialog.styles.jsx

This file was deleted.

94 changes: 94 additions & 0 deletions web/src/components/forums/Editor/RichTextEditor.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import React, {useState, useRef, useEffect} from 'react';
import {EditorState, RichUtils, convertFromRaw, convertToRaw} from 'draft-js';
import Editor, {composeDecorators} from '@draft-js-plugins/editor';
import createResizablePlugin from '@draft-js-plugins/resizeable';
import EditorToolbar from './Toolbar/EditorToolbar';
import createImagePlugin from '@draft-js-plugins/image';
import {makeStyles} from '@mui/styles';
import {Box, IconButton} from '@mui/material';
import CloseIcon from '@mui/icons-material/Close';

import 'draft-js/dist/Draft.css';
import './TextEditor.css';

const resizeablePlugin = createResizablePlugin();
const decorator = composeDecorators(
resizeablePlugin.decorator,
);
const imagePlugin = createImagePlugin({decorator});

const useStyles = makeStyles((theme) => ({
toolbarContainer: {
width: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
borderColor: theme.palette.white,
borderTop: theme.spacing(0.1) + ' solid',
borderBottom: theme.spacing(0.1) + ' solid',
},
editorContainer: {
width: '100%',
},
}));

/**
* Rich text editor used to display and publish data
* @param {Object} content - the content of the editor, use this when we need to solely display data
* @param {Function} setContent - the function to set the content of the editor, use this when we need to publish data
* @param {Function} setOpen - the function to set the open state of the editor, called when close button clicked
* @param {Boolean} readOnly - whether or not the editor is read only
* @param {Boolean} enableToolbar - whether or not to show the toolbar
* @returns {JSX.Element} - the rich text editor
*/
export default function RichTextEditor({content = null, setContent = null, setOpen = null, readOnly = false,
enableToolbar = true}) {
const [editorState, setEditorState] = useState(EditorState.createEmpty());
const editor = useRef(null);
const classes = useStyles();

if (!readOnly && setContent) {
useEffect(() => {
setContent(JSON.stringify(convertToRaw(editorState.getCurrentContent())));
}, [editorState]);
} else if (content) {
useEffect(() => {
setEditorState(EditorState.createWithContent(convertFromRaw(content)));
}, []);
}

const handleKeyCommand = (command) => {
const newState = RichUtils.handleKeyCommand(editorState, command);
if (newState) {
setEditorState(newState);
return true;
}
return false;
};
return (
<>
{enableToolbar &&
<Box className={classes.toolbarContainer}>
<EditorToolbar editorState={editorState} setEditorState={setEditorState} imagePlugin={imagePlugin} />
{
setOpen &&
<IconButton onClick={() => setOpen(false)}>
<CloseIcon />
</IconButton>
}
</Box>
}
<Editor
ref={editor}
className={classes.editorContainer}
handleKeyCommand={handleKeyCommand}
editorState={editorState}
onChange={(editorState) => {
setEditorState(editorState);
}}
readOnly={readOnly}
plugins={[imagePlugin, resizeablePlugin]}
/>
</>
);
};
Loading
Loading