diff --git a/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/submission/programming/SubmissionResource.java b/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/submission/programming/SubmissionResource.java index 5fda814ef..d1d6340e0 100644 --- a/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/submission/programming/SubmissionResource.java +++ b/judgels-backends/judgels-server-app/src/main/java/judgels/jerahmeel/submission/programming/SubmissionResource.java @@ -237,8 +237,9 @@ public Response getSubmissionSourceDarkImage(@PathParam("submissionJid") String @POST @Consumes(MULTIPART_FORM_DATA) + @Produces(APPLICATION_JSON) @UnitOfWork - public void createSubmission(@HeaderParam(AUTHORIZATION) AuthHeader authHeader, FormDataMultiPart parts) { + public Submission createSubmission(@HeaderParam(AUTHORIZATION) AuthHeader authHeader, FormDataMultiPart parts) { actorChecker.check(authHeader); String containerJid = checkNotNull(parts.getField("containerJid"), "containerJid").getValue(); @@ -255,6 +256,8 @@ public void createSubmission(@HeaderParam(AUTHORIZATION) AuthHeader authHeader, Submission submission = submissionClient.submit(data, source, config); submissionSourceBuilder.storeSubmissionSource(submission.getJid(), source); + + return submission; } @POST diff --git a/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.jsx b/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.jsx index 42440e300..e379a65ae 100644 --- a/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.jsx +++ b/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.jsx @@ -1,4 +1,4 @@ -import { HTMLTable, Button } from '@blueprintjs/core'; +import { HTMLTable, Button, ProgressBar } from '@blueprintjs/core'; import { Download } from '@blueprintjs/icons'; import { Link } from 'react-router-dom'; @@ -30,6 +30,8 @@ export function SubmissionDetails({ problemUrl, containerName, onDownload, + hideSourceFilename, + showLoaderWhenPending, }) { const hasSubtasks = latestGrading && latestGrading.details && latestGrading.details.subtaskResults.length > 1; @@ -146,6 +148,13 @@ export function SubmissionDetails({ )); }; + const renderLoader = () => { + if (showLoaderWhenPending && latestGrading?.verdict.code === VerdictCode.PND) { + return ; + } + return null; + }; + const renderSampleTestDataResults = () => { const details = latestGrading.details; if (details.testDataResults.length < 1) { @@ -302,9 +311,11 @@ export function SubmissionDetails({ const sourceFiles = Object.keys(submissionFiles).map(key => ( -
- {key === DEFAULT_SOURCE_KEY ? '' : key + ': '} {submissionFiles[key].name} -
+ {!hideSourceFilename && ( +
+ {key === DEFAULT_SOURCE_KEY ? '' : key + ': '} {submissionFiles[key].name} +
+ )} {decodeBase64(submissionFiles[key].content)} @@ -343,7 +354,7 @@ export function SubmissionDetails({ const renderSourceFilesHeading = () => { if (!onDownload) { - return

Source Files

; + return null; } return (
@@ -359,6 +370,7 @@ export function SubmissionDetails({ return (
{renderGeneralInfo()} + {renderLoader()} {renderDetails()} {renderSourceFiles()}
diff --git a/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.scss b/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.scss index bdb446728..b5da5b6fb 100644 --- a/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.scss +++ b/judgels-client/src/components/SubmissionDetails/Programming/SubmissionDetails.scss @@ -58,6 +58,10 @@ float: left; } + .pending-loader { + margin-bottom: 15px; + } + .submission-details-image { overflow: auto; } diff --git a/judgels-client/src/modules/jerahmeel/jerahmeelReducer.js b/judgels-client/src/modules/jerahmeel/jerahmeelReducer.js index 56ab472bf..92a64205f 100644 --- a/judgels-client/src/modules/jerahmeel/jerahmeelReducer.js +++ b/judgels-client/src/modules/jerahmeel/jerahmeelReducer.js @@ -5,6 +5,7 @@ import storage from 'redux-persist/es/storage'; import courseReducer from '../../routes/courses/courses/modules/courseReducer'; import courseChapterReducer from '../../routes/courses/courses/single/chapters/modules/courseChapterReducer'; import courseChaptersReducer from '../../routes/courses/courses/single/chapters/modules/courseChaptersReducer'; +import chapterProblemReducer from '../../routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemReducer'; import problemSetReducer from '../../routes/problems/problemsets/modules/problemSetReducer'; import problemSetProblemReducer from '../../routes/problems/problemsets/single/problems/modules/problemSetProblemReducer'; @@ -12,6 +13,7 @@ export default combineReducers({ course: persistReducer({ key: 'jerahmeelCourse', storage }, courseReducer), courseChapter: persistReducer({ key: 'jerahmeelCourseChapter', storage }, courseChapterReducer), courseChapters: courseChaptersReducer, + chapterProblem: chapterProblemReducer, problemSet: persistReducer({ key: 'jerahmeelProblemSet', storage }, problemSetReducer), problemSetProblem: persistReducer({ key: 'jerahmeelProblemSetProblem', storage }, problemSetProblemReducer), }); diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.jsx index 668ee23f4..245c3f4b4 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/ChapterProblemPage/ChapterProblemPage.jsx @@ -14,6 +14,7 @@ import { ProblemType } from '../../../../../../../../../modules/api/sandalphon/p import { selectCourse } from '../../../../../../modules/courseSelectors'; import { selectCourseChapter } from '../../../../modules/courseChapterSelectors'; import { selectCourseChapters } from '../../../../modules/courseChaptersSelectors'; +import { selectChapterProblemKey } from '../modules/chapterProblemSelectors'; import { selectStatementLanguage } from '../../../../../../../../../modules/webPrefs/webPrefsSelectors'; import * as chapterProblemActions from '../../modules/chapterProblemActions'; import * as breadcrumbsActions from '../../../../../../../../../modules/breadcrumbs/breadcrumbsActions'; @@ -25,33 +26,17 @@ export class ChapterProblemPage extends Component { response: undefined, }; - async componentDidMount() { - const response = await this.props.onGetProblemWorksheet( - this.props.chapter.jid, - this.props.match.params.problemAlias, - this.props.statementLanguage - ); - - this.setState({ - response, - }); - - this.props.onPushBreadcrumb(this.props.match.url, response.problem.alias); - - sendGAEvent({ category: 'Courses', action: 'View course problem', label: this.props.course.name }); - sendGAEvent({ category: 'Courses', action: 'View chapter problem', label: this.props.chapter.name }); - sendGAEvent({ - category: 'Courses', - action: 'View problem', - label: this.props.chapterName + ': ' + this.props.match.params.problemAlias, - }); + componentDidMount() { + this.refreshProblem(); } async componentDidUpdate(prevProps) { - if (this.props.statementLanguage !== prevProps.statementLanguage) { - await this.componentDidMount(); - } else if (this.props.match.params.problemAlias !== prevProps.match.params.problemAlias) { - await this.componentDidMount(); + if ( + this.props.statementLanguage !== prevProps.statementLanguage || + this.props.chapterProblemKey !== prevProps.chapterProblemKey || + this.props.match.params.problemAlias !== prevProps.match.params.problemAlias + ) { + await this.refreshProblem(); } } @@ -69,6 +54,28 @@ export class ChapterProblemPage extends Component { ); } + refreshProblem = async () => { + const response = await this.props.onGetProblemWorksheet( + this.props.chapter.jid, + this.props.match.params.problemAlias, + this.props.statementLanguage + ); + + this.setState({ + response, + }); + + this.props.onPushBreadcrumb(this.props.match.url, response.problem.alias); + + sendGAEvent({ category: 'Courses', action: 'View course problem', label: this.props.course.name }); + sendGAEvent({ category: 'Courses', action: 'View chapter problem', label: this.props.chapter.name }); + sendGAEvent({ + category: 'Courses', + action: 'View problem', + label: this.props.chapterName + ': ' + this.props.match.params.problemAlias, + }); + }; + renderHeader = () => { const { course, chapter, match } = this.props; @@ -148,6 +155,7 @@ const mapStateToProps = state => ({ course: selectCourse(state), chapter: selectCourseChapter(state), chapters: selectCourseChapters(state), + chapterProblemKey: selectChapterProblemKey(state), statementLanguage: selectStatementLanguage(state), }); const mapDispatchToProps = { diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.js b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.js index 9a281d540..c0bfde420 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.js +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/modules/chapterProblemSubmissionActions.js @@ -34,12 +34,20 @@ export function createSubmission(courseSlug, chapterJid, chapterAlias, problemJi sourceFiles['sourceFiles.' + key] = data.sourceFiles[key]; }); - await submissionProgrammingAPI.createSubmission(token, chapterJid, problemJid, data.gradingLanguage, sourceFiles); + const submission = await submissionProgrammingAPI.createSubmission( + token, + chapterJid, + problemJid, + data.gradingLanguage, + sourceFiles + ); toastActions.showSuccessToast('Solution submitted.'); window.scrollTo(0, 0); - dispatch(push(`/courses/${courseSlug}/chapters/${chapterAlias}/problems/${problemAlias}/submissions`)); + dispatch( + push(`/courses/${courseSlug}/chapters/${chapterAlias}/problems/${problemAlias}/submissions/${submission.id}`) + ); }; } diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.jsx b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.jsx index 4a86382ec..54fbf69a0 100644 --- a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.jsx +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/Programming/submissions/single/ChapterProblemSubmissionPage/ChapterProblemSubmissionPage.jsx @@ -10,6 +10,8 @@ import { SubmissionDetails } from '../../../../../../../../../../../../component import { selectStatementLanguage } from '../../../../../../../../../../../../modules/webPrefs/webPrefsSelectors'; import { selectCourse } from '../../../../../../../../../modules/courseSelectors'; import { selectCourseChapter } from '../../../../../../../modules/courseChapterSelectors'; +import { RefreshChapterProblem } from '../../../../modules/chapterProblemReducer'; +import { VerdictCode } from '../../../../../../../../../../../../modules/api/gabriel/verdict'; import * as breadcrumbsActions from '../../../../../../../../../../../../modules/breadcrumbs/breadcrumbsActions'; import * as chapterProblemSubmissionActions from '../../modules/chapterProblemSubmissionActions'; @@ -22,20 +24,10 @@ export class ChapterProblemSubmissionPage extends Component { containerName: undefined, }; - async componentDidMount() { - const { data, profile, problemName, containerName } = await this.props.onGetSubmissionWithSource( - +this.props.match.params.submissionId, - this.props.statementLanguage - ); - const sourceImageUrl = data.source ? undefined : await this.props.onGetSubmissionSourceImage(data.submission.jid); - this.props.onPushBreadcrumb(this.props.match.url, '#' + data.submission.id); - this.setState({ - submissionWithSource: data, - sourceImageUrl, - profile, - problemName, - containerName, - }); + currentTimeout; + + componentDidMount() { + this.refreshSubmission(); } async componentWillUnmount() { @@ -63,6 +55,36 @@ export class ChapterProblemSubmissionPage extends Component { ); } + refreshSubmission = async () => { + const { data, profile, problemName, containerName } = await this.props.onGetSubmissionWithSource( + +this.props.match.params.submissionId, + this.props.statementLanguage + ); + const sourceImageUrl = data.source ? undefined : await this.props.onGetSubmissionSourceImage(data.submission.jid); + this.props.onPushBreadcrumb(this.props.match.url, '#' + data.submission.id); + this.setState({ + submissionWithSource: data, + sourceImageUrl, + profile, + problemName, + containerName, + }); + + if (sourceImageUrl) { + return; + } + + const verdictCode = data.submission.latestGrading?.verdict.code || VerdictCode.PND; + if (verdictCode === VerdictCode.PND) { + this.currentTimeout = setTimeout(this.refreshSubmission, 1500); + } else { + if (this.currentTimeout) { + clearTimeout(this.currentTimeout); + this.props.onRefreshChapterProblem(Date.now()); + } + } + }; + renderSubmission = () => { const { submissionWithSource, profile, sourceImageUrl } = this.state; const { course, chapter } = this.props; @@ -79,6 +101,8 @@ export class ChapterProblemSubmissionPage extends Component { sourceImageUrl={sourceImageUrl} profile={profile} problemUrl={`/courses/${course.slug}/chapters/${chapter.alias}/problems/${problemAlias}`} + hideSourceFilename + showLoaderWhenPending /> ); }; @@ -91,6 +115,7 @@ const mapStateToProps = state => ({ }); const mapDispatchToProps = { + onRefreshChapterProblem: RefreshChapterProblem, onGetSubmissionWithSource: chapterProblemSubmissionActions.getSubmissionWithSource, onGetSubmissionSourceImage: chapterProblemSubmissionActions.getSubmissionSourceImage, onPushBreadcrumb: breadcrumbsActions.pushBreadcrumb, diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemReducer.js b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemReducer.js new file mode 100644 index 000000000..d5d45afdb --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemReducer.js @@ -0,0 +1,19 @@ +export const initialState = { + value: undefined, +}; + +export function RefreshChapterProblem(key) { + return { + type: 'jerahmeel/chapterProblem/REFRESH', + payload: key, + }; +} + +export default function chapterProblemReducer(state = initialState, action) { + switch (action.type) { + case 'jerahmeel/chapterProblem/REFRESH': + return { ...state, value: action.payload }; + default: + return state; + } +} diff --git a/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemSelectors.js b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemSelectors.js new file mode 100644 index 000000000..2c1a6ee7f --- /dev/null +++ b/judgels-client/src/routes/courses/courses/single/chapters/single/problems/single/modules/chapterProblemSelectors.js @@ -0,0 +1,3 @@ +export function selectChapterProblemKey(state) { + return state.jerahmeel.chapterProblem.value; +}