Skip to content

Commit

Permalink
fix: reworked grading deadline
Browse files Browse the repository at this point in the history
  • Loading branch information
ruzniaievdm committed Sep 4, 2023
1 parent e50b8c7 commit 7874c72
Show file tree
Hide file tree
Showing 33 changed files with 538 additions and 561 deletions.
2 changes: 2 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ coverage:
default:
target: auto
threshold: 0%
ignore:
- "src/grading-settings/grading-scale/react-ranger.js"
10 changes: 0 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,6 @@
"react-datepicker": "^4.13.0",
"react-dom": "16.14.0",
"react-helmet": "^6.1.0",
"react-ranger": "^2.1.0",
"react-redux": "7.1.3",
"react-responsive": "8.1.0",
"react-router": "5.2.0",
Expand Down
2 changes: 2 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,5 @@ export const NOTIFICATION_MESSAGES = {
duplicating: 'Duplicating',
deleting: 'Deleting',
};

export const DEFAULT_TIME_STAMP = '00:00';
4 changes: 2 additions & 2 deletions src/grading-settings/GradingSettings.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ describe('<GradingSettings />', () => {
expect(segmentInputs).toHaveLength(5);
const segmentInput = segmentInputs[1];
fireEvent.change(segmentInput, { target: { value: 'Test' } });
expect(segmentInput).toHaveValue('TEST');
expect(segmentInput).toHaveValue('Test');
expect(getByTestId('grading-settings-save-alert')).toBeVisible();
});
});
Expand All @@ -73,7 +73,7 @@ describe('<GradingSettings />', () => {
const segmentInput = segmentInputs[1];
fireEvent.change(segmentInput, { target: { value: 'Test' } });
fireEvent.click(getByText(messages.buttonCancelText.defaultMessage));
expect(segmentInput).toHaveValue('A');
expect(segmentInput).toHaveValue('a');
});
});
it('should save segment input changes and display saving message', async () => {
Expand Down
2 changes: 1 addition & 1 deletion src/grading-settings/assignment-section/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const messages = defineMessages({
},
assignmentTypeNameErrorMessage2: {
id: 'course-authoring.grading-settings.assignment.type-name.error.message-2',
defaultMessage: 'For grading to work, you must change all {initialAssignmentName} subsections to {value}',
defaultMessage: 'For grading to work, you must change all {initialAssignmentName} subsections to {value}.',
},
assignmentTypeNameErrorMessage3: {
id: 'course-authoring.grading-settings.assignment.type-name.error.message-3',
Expand Down
2 changes: 1 addition & 1 deletion src/grading-settings/assignment-section/utils/enum.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ export const defaultAssignmentsPropTypes = {
dropCount: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
shortLabel: PropTypes.string,
weight: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
id: PropTypes.number,
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
};
4 changes: 2 additions & 2 deletions src/grading-settings/data/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

import { deepConvertingKeysToSnakeCase } from '../../utils';
import { deepConvertingKeysToCamelCase, deepConvertingKeysToSnakeCase } from '../../utils';

const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL;
export const getGradingSettingsApiUrl = (courseId) => `${getApiBaseUrl()}/api/contentstore/v1/course_grading/${courseId}`;
Expand All @@ -16,7 +16,7 @@ export const getCourseSettingsApiUrl = (courseId) => `${getApiBaseUrl()}/api/con
export async function getGradingSettings(courseId) {
const { data } = await getAuthenticatedHttpClient()
.get(getGradingSettingsApiUrl(courseId));
return camelCaseObject(data);
return deepConvertingKeysToCamelCase(data);
}

/**
Expand Down
29 changes: 25 additions & 4 deletions src/grading-settings/deadline-section/DeadlineSection.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@ import React from 'react';
import { fireEvent, render, waitFor } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';

import DeadlineSection from '.';
import { DEFAULT_TIME_STAMP, TIME_FORMAT } from '../../constants';
import messages from './messages';
import DeadlineSection from '.';

const testObj = {};

Expand All @@ -19,7 +20,6 @@ const RootWrapper = (props = {}) => (
<IntlProvider locale="en">
<DeadlineSection
setShowSavePrompt={jest.fn()}
gracePeriod={gracePeriodDefaultTime}
setGradingData={jest.fn()}
setShowSuccessAlert={jest.fn()}
{...props}
Expand All @@ -29,14 +29,17 @@ const RootWrapper = (props = {}) => (

describe('<DeadlineSection />', () => {
it('checking deadline label and description text', async () => {
const { getByText } = render(<RootWrapper />);
const { getByText } = render(<RootWrapper gracePeriod={gracePeriodDefaultTime} />);
await waitFor(() => {
expect(getByText(messages.gracePeriodOnDeadlineLabel.defaultMessage)).toBeInTheDocument();
expect(getByText(messages.gracePeriodOnDeadlineDescription.defaultMessage)).toBeInTheDocument();
});
});
it('checking deadline input value', async () => {
const { getByTestId } = render(<RootWrapper setGradingData={setGradingData} />);
const { getByTestId } = render(<RootWrapper
gracePeriod={gracePeriodDefaultTime}
setGradingData={setGradingData}
/>);
await waitFor(() => {
const inputElement = getByTestId('deadline-period-input');
expect(inputElement.value).toBe('12:12');
Expand All @@ -45,4 +48,22 @@ describe('<DeadlineSection />', () => {
expect(testObj.gracePeriod.minutes).toBe(13);
});
});
it('checking deadline input value if grace Period equal null', async () => {
const { getByTestId } = render(<RootWrapper gracePeriod={null} setGradingData={setGradingData} />);
await waitFor(() => {
const inputElement = getByTestId('deadline-period-input');
expect(inputElement.value).toBe(DEFAULT_TIME_STAMP);
});
});
it('checking deadline input validation error', async () => {
const { getByPlaceholderText, getByText } = render(<RootWrapper
gracePeriod={gracePeriodDefaultTime}
setGradingData={setGradingData}
/>);
await waitFor(() => {
const inputElement = getByPlaceholderText(TIME_FORMAT.toUpperCase());
fireEvent.change(inputElement, { target: { value: 'wrong:input format' } });
expect(getByText(`Grace period must be specified in ${TIME_FORMAT.toUpperCase()} format.`)).toBeInTheDocument();
});
});
});
60 changes: 40 additions & 20 deletions src/grading-settings/deadline-section/index.jsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,66 @@
import React from 'react';
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import { Form } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import classNames from 'classnames';

import { DEFAULT_TIME_STAMP, TIME_FORMAT } from '../../constants';
import { formatTime, timerValidation } from './utils';
import messages from './messages';

const DEFAULT_TIME_STAMP = '00:00';

const DeadlineSection = ({
intl, setShowSavePrompt, gracePeriod, setGradingData, setShowSuccessAlert,
}) => {
const formatTime = (time) => (time >= 10 ? time.toString() : `0${time}`);
const timeStampValue = gracePeriod
? `${formatTime(gracePeriod.hours) }:${ formatTime(gracePeriod.minutes)}` : DEFAULT_TIME_STAMP;
? gracePeriod.hours && `${formatTime(gracePeriod.hours)}:${formatTime(gracePeriod.minutes)}`
: DEFAULT_TIME_STAMP;
const [newDeadlineValue, setNewDeadlineValue] = useState(timeStampValue);
const [isError, setIsError] = useState(false);

useEffect(() => {
setNewDeadlineValue(timeStampValue);
}, [gracePeriod]);

const handleDeadlineChange = (e) => {
const hoursAndMinutes = e.target.value.split(':');
setShowSavePrompt(true);
setGradingData(prevData => ({
...prevData,
gracePeriod: {
hours: Number(hoursAndMinutes[0]),
minutes: parseInt(hoursAndMinutes[1] ?? 0, 10),
},
}));
setShowSuccessAlert(false);
const { value } = e.target;
const [hours, minutes] = value.split(':');

setNewDeadlineValue(value);

if (timerValidation(value, setShowSavePrompt, setIsError)) {
setGradingData(prevData => ({
...prevData,
gracePeriod: {
hours: +hours,
minutes: +minutes,
},
}));
setShowSuccessAlert(false);
}
};

return (
<Form.Group className="w-50">
<Form.Group className={classNames('w-50 form-group-custom', {
'form-group-custom_isInvalid': isError,
})}
>
<Form.Label className="grading-label">
{intl.formatMessage(messages.gracePeriodOnDeadlineLabel)}
</Form.Label>
<Form.Control
data-testid="deadline-period-input"
type="time"
value={timeStampValue}
value={newDeadlineValue}
onChange={handleDeadlineChange}
placeholder={TIME_FORMAT.toUpperCase()}
/>
<Form.Control.Feedback className="grading-description">
{intl.formatMessage(messages.gracePeriodOnDeadlineDescription)}
</Form.Control.Feedback>
{isError && (
<Form.Control.Feedback className="feedback-error" type="invalid">
{intl.formatMessage(messages.gracePeriodOnDeadlineErrorMsg, { timeFormat: TIME_FORMAT.toUpperCase() })}
</Form.Control.Feedback>
)}
</Form.Group>
);
};
Expand All @@ -55,8 +75,8 @@ DeadlineSection.propTypes = {
setGradingData: PropTypes.func.isRequired,
setShowSuccessAlert: PropTypes.func.isRequired,
gracePeriod: PropTypes.shape({
hours: PropTypes.number,
minutes: PropTypes.number,
hours: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
minutes: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
}),
};

Expand Down
4 changes: 4 additions & 0 deletions src/grading-settings/deadline-section/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ const messages = defineMessages({
id: 'course-authoring.grading-settings.deadline.description',
defaultMessage: 'Leeway on due dates',
},
gracePeriodOnDeadlineErrorMsg: {
id: 'course-authoring.grading-settings.deadline.error.message',
defaultMessage: 'Grace period must be specified in {timeFormat} format.',
},
});

export default messages;
27 changes: 27 additions & 0 deletions src/grading-settings/deadline-section/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/**
* Time format conversions for values (hours or minutes) that are less than 10.
*
* @param {number} time - incoming time data.
* @returns {string} - formatted time string.
*/
export function formatTime(time) {
return (time >= 10 ? time.toString() : `0${time}`);
}

/**
* Validates inputStr as a time in HH:MM format.
*
* @param {string} inputStr - the input string to validate.
* @param {function} setShowSavePrompt - a function to control save prompt display.
* @param {function} setIsError - a function to set error state.
* @returns {boolean} - returns `true` if `inputStr` is a valid time, else `false`.
*/
export function timerValidation(inputStr, setShowSavePrompt, setIsError) {
const timePattern = /^\d{2,3}:\d{2}$/;

const isValid = timePattern.test(inputStr);
setShowSavePrompt(isValid);
setIsError(!isValid);

return isValid;
}
42 changes: 42 additions & 0 deletions src/grading-settings/deadline-section/utils.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { formatTime, timerValidation } from './utils';

const setShowSavePrompt = jest.fn();
const setIsError = jest.fn();

describe('formatTime', () => {
it('formats single-digit time values correctly', () => {
expect(formatTime(5)).toBe('05');
expect(formatTime(9)).toBe('09');
});

it('does not modify double-digit time values', () => {
expect(formatTime(10)).toBe('10');
expect(formatTime(45)).toBe('45');
});
});

describe('timerValidation', () => {
it('handles empty input', () => {
const result = timerValidation('', setShowSavePrompt, setIsError);

expect(result).toBe(false);
expect(setShowSavePrompt).toHaveBeenCalledWith(false);
expect(setIsError).toHaveBeenCalledWith(true);
});

it('validates correct HH:MM input', () => {
const result = timerValidation('12:34', setShowSavePrompt, setIsError);

expect(result).toBe(true);
expect(setShowSavePrompt).toHaveBeenCalledWith(true);
expect(setIsError).toHaveBeenCalledWith(false);
});

it('handles invalid input', () => {
const result = timerValidation('abc', setShowSavePrompt, setIsError);

expect(result).toBe(false);
expect(setShowSavePrompt).toHaveBeenCalledWith(false);
expect(setIsError).toHaveBeenCalledWith(true);
});
});
5 changes: 3 additions & 2 deletions src/grading-settings/grading-scale/GradingScale.jsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { useRanger } from 'react-ranger';
import { Icon, IconButton } from '@edx/paragon';
import { Add as IconAdd } from '@edx/paragon/icons';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';

import { useRanger } from './react-ranger';
import messages from './messages';
import { convertGradeData, MAXIMUM_SCALE_LENGTH } from './utils';
import { GradingScaleTicks, GradingScaleHandle, GradingScaleSegment } from './components';

const DEFAULT_LETTERS = ['A', 'B', 'C', 'D'];
const getDefaultPassText = intl => intl.formatMessage(messages.defaultPassText);

const GradingScale = ({
intl,
Expand Down Expand Up @@ -144,7 +145,7 @@ const GradingScale = ({
const updatedLetters = [...prevLetters];
updatedLetters.splice(updatedLetters.length - 1, 1);

return updatedLetters.length === 1 ? ['pass'] : updatedLetters;
return updatedLetters.length === 1 ? [getDefaultPassText(intl)] : updatedLetters;
});
};

Expand Down
4 changes: 2 additions & 2 deletions src/grading-settings/grading-scale/GradingScale.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ describe('<GradingScale />', () => {
expect(segmentInputs).toHaveLength(5);
const segmentInput = segmentInputs[1];
fireEvent.change(segmentInput, { target: { value: 'Test' } });
expect(segmentInput).toHaveValue('TEST');
expect(segmentInput).toHaveValue('Test');
});
});

Expand Down Expand Up @@ -120,7 +120,7 @@ describe('<GradingScale />', () => {
const segmentInputs = getAllByTestId('grading-scale-segment-input');
expect(segmentInputs[0]).toHaveValue('Fail');
fireEvent.change(segmentInputs[1], { target: { value: 'Test' } });
expect(segmentInputs[1]).toHaveValue('TEST');
expect(segmentInputs[1]).toHaveValue('Test');
});
});
});
4 changes: 4 additions & 0 deletions src/grading-settings/grading-scale/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ const messages = defineMessages({
id: 'course-authoring.grading-settings.fail-segment.text',
defaultMessage: 'Fail',
},
defaultPassText: {
id: 'course-authoring.grading-settings.default.pass.text',
defaultMessage: 'Pass',
},
});

export default messages;
Loading

0 comments on commit 7874c72

Please sign in to comment.