Skip to content

Commit

Permalink
feat: Added remote status chip to each file in the selection for comm…
Browse files Browse the repository at this point in the history
…it file list.

ref: #8
  • Loading branch information
mpetojevic committed Feb 28, 2024
1 parent fe912a0 commit c845a4c
Show file tree
Hide file tree
Showing 9 changed files with 229 additions and 7 deletions.
40 changes: 40 additions & 0 deletions grader_labextension/handlers/version_control.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,46 @@ async def put(self, lecture_id: int, assignment_id: int):
self.write("OK")


@register_handler(
path=r"\/lectures\/(?P<lecture_id>\d*)\/assignments\/(?P<assignment_id>\d*)\/remote-file-status\/(?P<repo>\w*)\/?"
)
class GitRemoteFileStatusHandler(ExtensionBaseHandler):
"""
Tornado Handler class for http requests to /lectures/{lecture_id}/assignments/{assignment_id}/remote-file-status/{repo}.
"""

@cache(max_age=15)
async def get(self, lecture_id: int, assignment_id: int, repo: str):
if repo not in {"assignment", "source", "release"}:
self.log.error(HTTPStatus.NOT_FOUND)
raise HTTPError(
HTTPStatus.NOT_FOUND, reason=f"Repository {repo} does not exist"
)
lecture = await self.get_lecture(lecture_id)
assignment = await self.get_assignment(lecture_id, assignment_id)
file_path = self.get_query_argument('file') # Retrieve the file path from the query parameters
git_service = GitService(
server_root_dir=self.root_dir,
lecture_code=lecture["code"],
assignment_id=assignment["id"],
repo_type=repo,
config=self.config,
force_user_repo=True if repo == "release" else False,
)
try:
if not git_service.is_git():
git_service.init()
git_service.set_author(author=self.user_name)
git_service.set_remote(f"grader_{repo}")
git_service.fetch_all()
status = git_service.check_remote_file_status(file_path)
self.log.info(f"File {file_path} status: {status}")
except GitError as e:
self.log.error(e)
raise HTTPError(HTTPStatus.INTERNAL_SERVER_ERROR, reason=str(e))
self.write(status.name)


@register_handler(
path=r"\/lectures\/(?P<lecture_id>\d*)\/assignments\/(?P<assignment_id>\d*)\/remote-status\/(?P<repo>\w*)\/?"
)
Expand Down
18 changes: 18 additions & 0 deletions grader_labextension/services/git.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ class RemoteStatus(enum.Enum):
push_needed = 3
divergent = 4

class RemoteFileStatus(enum.Enum):
up_to_date = 1
push_needed = 2
divergent = 3

class GitService(Configurable):
git_access_token = Unicode(os.environ.get("JUPYTERHUB_API_TOKEN"), allow_none=False).tag(config=True)
Expand Down Expand Up @@ -319,6 +323,20 @@ def git_status(self, hidden_files: bool = False) -> Tuple[List[str], List[str],
elif k == "D":
deleted.append(v)
return untracked, added, modified, deleted

def check_remote_file_status(self, file_path: str) -> RemoteFileStatus:
file_status_list = self._run_command(f"git status --porcelain {file_path}", cwd=self.path, capture_output=True).split(maxsplit=1)
# Extract the status character from the list
if file_status_list:
file_status = file_status_list[0]
else:
# If the list is empty, the file is up-to-date
return RemoteFileStatus.up_to_date
# Convert the file status to the corresponding enum value
if file_status in {"??", "M", "A", "D"}:
return RemoteFileStatus.push_needed
else:
return RemoteFileStatus.divergent

def local_branch_exists(self, branch: str) -> bool:
ret_code = self._run_command(f"git rev-parse --quiet --verify {branch}", cwd=self.path, check=False).returncode
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"@jupyterlab/mainmenu": "^4.0.11",
"@jupyterlab/notebook": "^4.0.2",
"@jupyterlab/services": "^7.0.0",
"@jupyterlab/git": "^0.50.0",
"@jupyterlab/settingregistry": "^4.0.0",
"@jupyterlab/terminal": "^4.0.2",
"@lumino/widgets": "^2.1.1",
Expand Down
4 changes: 3 additions & 1 deletion src/components/coursemanage/files/files.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ export const Files = (props: IFilesProps) => {
* @param commitMessage the commit message
*/
const handlePushAssignment = async (commitMessage: string, selectedFiles: string[]) => {
console.log("Files to commit: " + selectedFiles);
// console.log("Files to commit: " + selectedFiles);
showDialog(
'Push Assignment',
`Do you want to push ${assignment.name}? This updates the state of the assignment on the server with your local state.`,
Expand Down Expand Up @@ -363,6 +363,8 @@ export const Files = (props: IFilesProps) => {
<Box>
<FilesList
path={`${lectureBasePath}${props.lecture.code}/${selectedDir}/${props.assignment.id}`}
lecture={props.lecture}
assignment={props.assignment}
checkboxes={false}
/>
</Box>
Expand Down
61 changes: 58 additions & 3 deletions src/components/util/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ import {
TooltipProps,
tooltipClasses,
Typography,
Snackbar
Snackbar,
Modal,
Alert,
AlertTitle
} from '@mui/material';
import { Assignment } from '../../model/assignment';
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
Expand All @@ -55,6 +58,7 @@ import { updateMenus } from '../../menu';
import { GraderLoadingButton } from './loading-button';
import { FilesList } from './file-list';
import { extractRelativePaths, getFiles, lectureBasePath } from '../../services/file.service';
import InfoIcon from '@mui/icons-material/Info';

const gradingBehaviourHelp = `Specifies the behaviour when a students submits an assignment.\n
No Automatic Grading: No action is taken on submit.\n
Expand Down Expand Up @@ -496,6 +500,49 @@ export const CreateDialog = (props: ICreateDialogProps) => {
);
};

const InfoModal = () => {
const [open, setOpen] = React.useState(false);
const handleOpen = () => {
setOpen(true);
};
const handleClose = () => {
setOpen(false);
};
return (
<React.Fragment>
<IconButton color="primary" onClick={handleOpen} sx={{ mr: 2 }}>
<InfoIcon />
</IconButton>
<Modal open={open} onClose={handleClose}>
<Box
sx={{ position: 'absolute' as const,
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '80%',
bgcolor: 'background.paper',
boxShadow: 3,
pt: 2,
px: 4,
pb: 3
}}
>
<h2>Manual Grading Information</h2>
<Alert severity="info" sx={{ m: 2 }}>
<AlertTitle>Info</AlertTitle>
If you have made changes to multiple files in your source directory and wish to push only specific
files to the remote repository, you can toggle the 'Select files to commit' button. This allows you to
choose the files you want to push. Your students will then be able to view only the changes in files you have selected.
If you do not use this option, all changed files from the source repository will be pushed, and students will see all the changes
</Alert>
<Button onClick={handleClose}>Close</Button>
</Box>
</Modal>
</React.Fragment>
);
};


export interface ICommitDialogProps {
handleCommit: (msg: string, selectedFiles?: string[]) => void;
children: React.ReactNode;
Expand Down Expand Up @@ -552,14 +599,22 @@ export const CommitDialog = (props: ICommitDialogProps) => {
fullWidth={true}
maxWidth={'sm'}
>
<DialogTitle>Commit Files</DialogTitle>
<Stack direction={'row'} justifyContent={'space-between'}>
<DialogTitle>Commit Files</DialogTitle>
<InfoModal />
</Stack>
<DialogContent>
<Button onClick={toggleFilesList} sx={{ mb: 2 }}>
{filesListVisible ? <KeyboardArrowUpIcon /> : <KeyboardArrowRightIcon />}
Choose files to commit
</Button>
{filesListVisible && (
<FilesList path={path} checkboxes={true} onFileSelectChange={handleFileSelectChange} />
<FilesList
path={path}
lecture={props.lecture}
assignment={props.assignment}
checkboxes={true}
onFileSelectChange={handleFileSelectChange} />
)}
<TextField
sx={{ mt: 2, width: '100%' }}
Expand Down
87 changes: 84 additions & 3 deletions src/components/util/file-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,26 @@ import {
Tooltip,
Stack,
Typography,
Checkbox
Checkbox,
Chip
} from '@mui/material';
import InsertDriveFileRoundedIcon from '@mui/icons-material/InsertDriveFileRounded';
import WarningIcon from '@mui/icons-material/Warning';
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import { Contents } from '@jupyterlab/services';
import DangerousIcon from '@mui/icons-material/Dangerous';
import { File, getRelativePath } from '../../services/file.service';
import { File, getRelativePath, getRemoteFileStatus } from '../../services/file.service';
import { Lecture } from '../../model/lecture';
import { Assignment } from '../../model/assignment';
import { RepoType } from './repo-type';
import CompareArrowsIcon from '@mui/icons-material/CompareArrows';
import CheckIcon from '@mui/icons-material/Check';
import PublishRoundedIcon from '@mui/icons-material/PublishRounded';

interface IFileItemProps {
file: File;
lecture?: Lecture,
assignment?: Assignment;
inContained: (file: string) => boolean;
missingFiles?: File[];
openFile: (path: string) => void;
Expand All @@ -28,18 +37,86 @@ interface IFileItemProps {

const FileItem = ({
file,
lecture,
assignment,
inContained,
openFile,
allowFiles,
missingFiles,
checkboxes,
onFileSelectChange
}: IFileItemProps) => {

const inMissing = (filePath: string) => {
return missingFiles.some(missingFile => missingFile.path === filePath);
};

const [isSelected, setIsSelected] = React.useState(true);
const [fileRemoteStatus, setFileRemoteStatus] = React.useState(
null as 'up_to_date' | 'push_needed' | 'divergent'
);

React.useEffect(() => {
getRemoteFileStatus(
lecture,
assignment,
RepoType.SOURCE,
getRelativePath(file.path, 'source'),
true
).then(status => {
setFileRemoteStatus(
status as 'up_to_date' | 'push_needed' | 'divergent'
);
});
}, [assignment, lecture]);

const getFleRemoteStatusText = (
status: 'up_to_date' | 'push_needed' | 'divergent'
) => {
if (status === 'up_to_date') {
return 'The local files is up to date with the file from remote repository.'
} else if (status === 'push_needed') {
return 'You have made changes to this file locally, a push is needed.';
} else {
return 'The local and remote file are divergent.';
}
};

const getStatusChip = (
status: 'up_to_date' | 'push_needed' | 'divergent'
) => {
if (status === 'up_to_date') {
return (
<Chip
sx={{ mb: 1.0 }}
label={'Up To Date'}
color="success"
size="small"
icon={<CheckIcon />}
/>
);
} else if (status === 'push_needed') {
return (
<Chip
sx={{ mb: 1.0 }}
label={'Push Needed'}
color="warning"
size="small"
icon={<PublishRoundedIcon />}
/>
);
} else {
return (
<Chip
sx={{ mb: 1.0 }}
label={'Divergent'}
color="error"
size="small"
icon={<CompareArrowsIcon />}
/>
);
}
};

const toggleSelection = () => {
setIsSelected(prevState => {
Expand All @@ -55,7 +132,6 @@ const FileItem = ({
const missingFileHelp =
'This file should be part of your assignment! Did you delete it?';


return (
<ListItem disablePadding>
{checkboxes && (
Expand All @@ -77,6 +153,11 @@ const FileItem = ({
primary={<Typography>{file.name}</Typography>}
secondary={
<Stack direction={'row'} spacing={2}>
{checkboxes && (
<Tooltip title={getFleRemoteStatusText(fileRemoteStatus)}>
{getStatusChip(fileRemoteStatus)}
</Tooltip>
)}
{inMissing(file.path) && (
<Tooltip title={missingFileHelp}>
<Stack direction={'row'} spacing={2} flex={0}>
Expand Down
4 changes: 4 additions & 0 deletions src/components/util/file-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ export const FilesList = (props: IFileListProps) => {
<FolderItem
key={file.path}
folder={file}
lecture={props.lecture}
assigment={props.assignment}
missingFiles={missingFiles || []}
inContained={inContained}
openFile={openFile}
Expand All @@ -100,6 +102,8 @@ export const FilesList = (props: IFileListProps) => {
<FileItem
key={file.path}
file={file}
lecture={props.lecture}
assignment={props.assignment}
missingFiles={missingFiles || []}
inContained={inContained}
openFile={openFile}
Expand Down
10 changes: 10 additions & 0 deletions src/components/util/folder-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@ import FileItem from './file-item';
import KeyboardArrowRightIcon from '@mui/icons-material/KeyboardArrowRight';
import KeyboardArrowUpIcon from '@mui/icons-material/KeyboardArrowUp';
import { File, getFiles } from '../../services/file.service';
import { Lecture } from '../../model/lecture';
import { Assignment } from '../../model/assignment';

interface IFolderItemProps {
folder: File;
lecture?: Lecture;
assignment?: Assignment;
inContained: (file: string) => boolean;
openFile: (path: string) => void;
allowFiles?: boolean;
Expand All @@ -29,6 +33,8 @@ interface IFolderItemProps {

const FolderItem = ({
folder,
lecture,
assigment,
missingFiles,
inContained,
openFile,
Expand Down Expand Up @@ -88,6 +94,8 @@ const FolderItem = ({
<FolderItem
key={file.path}
folder={file}
lecture={lecture}
assigment={assigment}
missingFiles={missingFiles || []}
inContained={inContained}
openFile={openFile}
Expand All @@ -98,6 +106,8 @@ const FolderItem = ({
<FileItem
key={file.path}
file={file}
lecture={lecture}
assignment={assigment}
missingFiles={missingFiles || []}
inContained={inContained}
openFile={openFile}
Expand Down
Loading

0 comments on commit c845a4c

Please sign in to comment.