diff --git a/app/controllers/course/assessment/submission/submissions_controller.rb b/app/controllers/course/assessment/submission/submissions_controller.rb index 40d133a9210..7a03e92183a 100644 --- a/app/controllers/course/assessment/submission/submissions_controller.rb +++ b/app/controllers/course/assessment/submission/submissions_controller.rb @@ -108,17 +108,17 @@ def generate_live_feedback end def set_timer_started_at - timer_started_at = Time.zone.now + unless @submission.timer_started_at + @submission.timer_started_at = Time.zone.now - @submission.timer_started_at = timer_started_at + raise ActiveRecord::Rollback unless @submission.save - raise ActiveRecord::Rollback unless @submission.save - - Course::Assessment::Submission::ForceSubmitTimedSubmissionJob. - set(wait_until: timer_started_at + @assessment.time_limit.minutes + FORCE_SUBMIT_DELAY). - perform_later(@assessment, @submission_id, @submission.creator) + Course::Assessment::Submission::ForceSubmitTimedSubmissionJob. + set(wait_until: @submission.timer_started_at + @assessment.time_limit.minutes + FORCE_SUBMIT_DELAY). + perform_later(@assessment, @submission_id, @submission.creator) + end - render json: { timerStartedAt: timer_started_at } + render json: { timerStartedAt: @submission.timer_started_at } end # Reload the current answer or reset it, depending on parameters. diff --git a/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/TimeLimitBanner.tsx b/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/TimeLimitBanner.tsx index d6122cbc9ff..5f7dc844ca1 100644 --- a/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/TimeLimitBanner.tsx +++ b/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/TimeLimitBanner.tsx @@ -1,22 +1,38 @@ import { FC, useEffect, useState } from 'react'; -import { FormattedMessage } from 'react-intl'; import { HourglassTop } from '@mui/icons-material'; +import { Typography } from '@mui/material'; import Banner from 'lib/components/core/layouts/Banner'; +import { useAppSelector } from 'lib/hooks/store'; +import useTranslation from 'lib/hooks/useTranslation'; import { BUFFER_TIME_TO_FORCE_SUBMIT_MS } from '../../constants'; +import { getAssessment } from '../../selectors/assessments'; +import { getSubmission } from '../../selectors/submissions'; import translations from '../../translations'; import RemainingTimeTranslations from './components/RemainingTimeTranslation'; -interface Props { - submissionTimeLimitAt: number; -} +const TimeLimitBanner: FC = () => { + const { t } = useTranslation(); + + const assessment = useAppSelector(getAssessment); + const submission = useAppSelector(getSubmission); -const TimeLimitBanner: FC = (props) => { - const { submissionTimeLimitAt } = props; const initialCurrentTime = new Date().getTime(); - const initialRemainingTime = submissionTimeLimitAt - initialCurrentTime; + const hasSubmissionTimeLimit = + assessment.timeLimit && + submission.workflowState === 'attempting' && + submission.timerStartedAt; + + const submissionTimeLimitAt = hasSubmissionTimeLimit + ? new Date(submission.timerStartedAt).getTime() + + assessment.timeLimit! * 60 * 1000 + : null; + + const initialRemainingTime = submissionTimeLimitAt + ? submissionTimeLimitAt - initialCurrentTime + : assessment.timeLimit! * 60 * 1000; const [currentRemainingTime, setCurrentRemainingTime] = useState(initialRemainingTime); @@ -25,63 +41,61 @@ const TimeLimitBanner: FC = (props) => { ); useEffect(() => { - const interval = setInterval(() => { - const currentTime = new Date().getTime(); - const remainingSeconds = submissionTimeLimitAt - currentTime; - const remainingBufferSeconds = - submissionTimeLimitAt + BUFFER_TIME_TO_FORCE_SUBMIT_MS - currentTime; + if (submissionTimeLimitAt) { + const interval = setInterval(() => { + const currentTime = new Date().getTime(); + const remainingSeconds = submissionTimeLimitAt - currentTime; + const remainingBufferSeconds = + submissionTimeLimitAt + BUFFER_TIME_TO_FORCE_SUBMIT_MS - currentTime; - setCurrentRemainingTime(remainingSeconds); + setCurrentRemainingTime(remainingSeconds); - if (remainingSeconds < 0) { - setCurrentBufferTime(remainingBufferSeconds); - } - }, 1000); + if (remainingSeconds < 0) { + setCurrentBufferTime(remainingBufferSeconds); + } + }, 1000); - return () => clearInterval(interval); - }, [submissionTimeLimitAt]); + return () => clearInterval(interval); + } - let TimeBanner: JSX.Element; + return () => {}; + }, [submissionTimeLimitAt]); - if (currentRemainingTime > 0) { - TimeBanner = ( - } - > - - ), - }} - /> - - ); - } else { - TimeBanner = ( + if (currentRemainingTime <= 0) { + return ( } > {currentBufferTime > 0 ? ( - + {t(translations.remainingBufferTime, { timeLimit: ( ), - }} - /> + })} + ) : ( - + {t(translations.timeIsUp)} )} ); } - return TimeBanner; + return ( + } + > + + {t(translations.remainingTime, { + timeLimit: ( + + ), + })} + + + ); }; export default TimeLimitBanner; diff --git a/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/index.jsx b/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/index.jsx index 2ab0e569acd..3d5f036ac66 100644 --- a/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/index.jsx +++ b/client/app/bundles/course/assessment/submission/pages/SubmissionEditIndex/index.jsx @@ -1,7 +1,6 @@ import { Component } from 'react'; import { FormattedMessage, injectIntl } from 'react-intl'; import { connect } from 'react-redux'; -import { HourglassTop } from '@mui/icons-material'; import InsertDriveFile from '@mui/icons-material/InsertDriveFile'; import { Card, @@ -15,7 +14,6 @@ import { import PropTypes from 'prop-types'; import withHeartbeatWorker from 'workers/withHeartbeatWorker'; -import Banner from 'lib/components/core/layouts/Banner'; import Page from 'lib/components/core/layouts/Page'; import Link from 'lib/components/core/Link'; import LoadingIndicator from 'lib/components/core/LoadingIndicator'; @@ -30,7 +28,6 @@ import { purgeSubmissionStore, } from '../../actions'; import ProgressPanel from '../../components/ProgressPanel'; -import { workflowStates } from '../../constants'; import { assessmentShape, gradingShape, @@ -39,7 +36,6 @@ import { } from '../../propTypes'; import translations from '../../translations'; -import RemainingTimeTranslations from './components/RemainingTimeTranslation'; import BlockedSubmission from './BlockedSubmission'; import SubmissionEmptyForm from './SubmissionEmptyForm'; import SubmissionForm from './SubmissionForm'; @@ -69,30 +65,11 @@ class VisibleSubmissionEditIndex extends Component { } renderTimeLimitBanner() { - const { assessment, submission, submissionTimeLimitAt } = this.props; + const { assessment, submission } = this.props; return ( assessment.timeLimit && - submission.workflowState === 'attempting' && - (submission.timerStartedAt ? ( - - ) : ( - } - > - - ), - }} - /> - - )) + submission.workflowState === 'attempting' && ); } @@ -204,7 +181,6 @@ VisibleSubmissionEditIndex.propTypes = { }), }), assessment: assessmentShape, - submissionTimeLimitAt: PropTypes.number, intl: PropTypes.object.isRequired, submission: submissionShape, isLoading: PropTypes.bool.isRequired, @@ -217,18 +193,8 @@ VisibleSubmissionEditIndex.propTypes = { }; function mapStateToProps({ assessments: { submission } }) { - const hasSubmissionTimeLimit = - submission.submission.workflowState === workflowStates.Attempting && - submission.assessment.timeLimit && - submission.submission.timerStartedAt; - const submissionTimeLimitAt = hasSubmissionTimeLimit - ? new Date(submission.submission.timerStartedAt).getTime() + - submission.assessment.timeLimit * 60 * 1000 - : null; - return { assessment: submission.assessment, - submissionTimeLimitAt, submission: submission.submission, isLoading: submission.submissionFlags.isLoading, isSaving: submission.submissionFlags.isSaving, diff --git a/spec/controllers/course/assessment/submission/submissions_controller_spec.rb b/spec/controllers/course/assessment/submission/submissions_controller_spec.rb index c89b7957b82..d279f77a5b2 100644 --- a/spec/controllers/course/assessment/submission/submissions_controller_spec.rb +++ b/spec/controllers/course/assessment/submission/submissions_controller_spec.rb @@ -240,6 +240,55 @@ end end + describe '#set_timer_started_at' do + let!(:assessment) { create(:assessment, :published, *assessment_traits, course: course, time_limit: 120) } + let!(:assessment2) { create(:assessment, :published, *assessment_traits, course: course, time_limit: 120) } + let!(:submission) { create(:submission, :attempting, assessment: assessment, creator: user) } + let!(:submission2) do + create(:submission, :attempting, assessment: assessment2, creator: user, + timer_started_at: Time.zone.now - 5.seconds) + end + + context 'when user first-time attempt the timed assessment' do + subject do + patch :set_timer_started_at, params: { + course_id: course, assessment_id: assessment.id, id: submission.id + } + end + + it 'assigns the timer_started_at to current time' do + subject + json_result = JSON.parse(response.body) + expect(json_result['timerStartedAt'].to_datetime.utc).to \ + be_within(1.second).of Time.zone.now.utc + expect(submission.reload.timer_started_at.utc).to \ + be_within(1.second).of Time.zone.now.utc + end + end + + context 'when user has already attempted the timed assessment before' do + subject do + patch :set_timer_started_at, params: { + course_id: course, assessment_id: assessment2.id, id: submission2.id + } + end + + it 'assigns the timer_started_at to current time' do + subject + json_result = JSON.parse(response.body) + expect(json_result['timerStartedAt'].to_datetime.utc).not_to \ + be_within(1.second).of Time.zone.now.utc + expect(submission2.reload.timer_started_at.utc).not_to \ + be_within(1.second).of Time.zone.now.utc + + expect(json_result['timerStartedAt'].to_datetime.utc).to \ + be_within(1.second).of (Time.zone.now - 5.seconds).utc + expect(submission2.reload.timer_started_at.utc).to \ + be_within(1.second).of (Time.zone.now - 5.seconds).utc + end + end + end + describe 'submission_actions' do let!(:students) { create_list(:course_student, 5, course: course) } let!(:phantom_student) { create(:course_student, :phantom, course: course) }