Skip to content

Commit

Permalink
feat(programming): expose programming language dependencies
Browse files Browse the repository at this point in the history
  • Loading branch information
adi-herwana-nus committed Jan 21, 2025
1 parent 6a5144c commit 71d3fce
Show file tree
Hide file tree
Showing 13 changed files with 257 additions and 4 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ json.array! languages do |language|
json.defaultEvaluator language.default_evaluator_whitelisted?
json.codaveriEvaluator language.codaveri_evaluator_whitelisted?
end
json.dependencies language.class.dependencies
json.editorMode language.ace_mode
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Typography } from '@mui/material';
import { LanguageDependencyData } from 'types/course/assessment/question/programming';

import Prompt from 'lib/components/core/dialogs/Prompt';
import useTranslation from 'lib/hooks/useTranslation';
import formTranslations from 'lib/translations/form';

import InstalledDependenciesTable from './InstalledDependenciesTable';

interface InstalledDependenciesProps {
disabled?: boolean;
open: boolean;
onClose: () => void;
title: string;
description: string;
dependencies: LanguageDependencyData[];
}

const InstalledDependenciesPrompt = (
props: InstalledDependenciesProps,
): JSX.Element => {
const { t } = useTranslation();
return (
<Prompt
cancelColor="info"
cancelLabel={t(formTranslations.close)}
disabled={props.disabled}
maxWidth="lg"
onClose={props.onClose}
open={props.open}
title={props.title}
>
<Typography variant="body2"> {props.description} </Typography>

<InstalledDependenciesTable
className="mt-2"
dependencies={props.dependencies}
/>
</Prompt>
);
};

export default InstalledDependenciesPrompt;
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import { LanguageDependencyData } from 'types/course/assessment/question/programming';

import translations from 'course/assessment/translations';
import Link from 'lib/components/core/Link';
import Table, { ColumnTemplate } from 'lib/components/table';
import useTranslation from 'lib/hooks/useTranslation';
import tableTranslations from 'lib/translations/table';

interface InstalledDependenciesTableProps {
className?: string;
dependencies: LanguageDependencyData[];
}

const InstalledDependenciesTable = (
props: InstalledDependenciesTableProps,
): JSX.Element => {
const { dependencies } = props;
const { t } = useTranslation();

const columns: ColumnTemplate<LanguageDependencyData>[] = [
{
of: 'name',
title: t(tableTranslations.name),
sortable: true,
searchable: true,
cell: (dependency: LanguageDependencyData): string | JSX.Element => {
const title = dependency.title ?? dependency.name;
if (dependency.href) {
return (
<Link href={dependency.href} opensInNewTab underline="hover">
{title}
</Link>
);
}
return title;
},
searchProps: {
getValue: (datum: LanguageDependencyData): string => {
const keywords = [datum.name];
if (datum.title) keywords.push(datum.title);
if (datum.aliases) keywords.push(...datum.aliases);
return keywords.join(', ');
},
},
},
{
of: 'version',
title: t(translations.dependencyVersionTableHeading),
cell: (dependency) => dependency.version,
},
];

return (
<Table
className={props.className}
columns={columns}
data={dependencies}
getRowId={(dependency): string =>
`${dependency.name} ${dependency.version}`
}
indexing={{ indices: false }}
search={{
searchPlaceholder: t(translations.dependencySearchText),
}}
toolbar={{
show: true,
}}
/>
);
};

export default InstalledDependenciesTable;
Original file line number Diff line number Diff line change
@@ -1,39 +1,76 @@
import { useState } from 'react';
import { Controller, useFormContext } from 'react-hook-form';
import { Grid, InputAdornment, RadioGroup, Typography } from '@mui/material';
import {
LanguageData,
LanguageDependencyData,
ProgrammingFormData,
} from 'types/course/assessment/question/programming';

import RadioButton from 'lib/components/core/buttons/RadioButton';
import ExperimentalChip from 'lib/components/core/ExperimentalChip';
import Subsection from 'lib/components/core/layouts/Subsection';
import Link from 'lib/components/core/Link';
import FormCheckboxField from 'lib/components/form/fields/CheckboxField';
import FormTextField from 'lib/components/form/fields/TextField';
import { SUPPORT_EMAIL } from 'lib/constants/sharedConstants';
import useTranslation from 'lib/hooks/useTranslation';

import translations from '../../../../translations';
import { useProgrammingFormDataContext } from '../../hooks/ProgrammingFormDataContext';
import InstalledDependenciesPrompt from '../common/InstalledDependenciesPrompt';

interface EvaluatorFieldsProps {
disabled?: boolean;
getDataFromId: (id: number) => LanguageData;
}

interface LanguageDependencyState {
title: string;
description: string;
dependencies: LanguageDependencyData[];
}

const EvaluatorFields = (props: EvaluatorFieldsProps): JSX.Element | null => {
const { t } = useTranslation();

const { control, watch } = useFormContext<ProgrammingFormData>();

const { question } = useProgrammingFormDataContext();

const [dependenciesOpen, setDependenciesOpen] = useState(false);
const [dependencyState, setDependencyState] =
useState<LanguageDependencyState>({
title: '',
description: '',
dependencies: [],
});

const currentLanguage = props.getDataFromId(watch('question.languageId'));
const autograded = watch('question.autograded');
if (!autograded) return null;

const autogradedAssessment = question.autogradedAssessment;
const codaveriDisabled = !question.codaveriEnabled;

const openEvaluatorDependencyPrompt = (): void => {
setDependencyState({
title: t(translations.defaultEvaluatorDependencyTitle, {
name: currentLanguage.name,
}),
description: t(translations.defaultEvaluatorDependencyDescription, {
br: <br />,
mailto: (chunk: string): JSX.Element => (
<Link external href={`mailto:${SUPPORT_EMAIL}`}>
{chunk}
</Link>
),
}),
dependencies: currentLanguage.dependencies,
});
setDependenciesOpen(true);
};

return (
<>
<Subsection className="!mt-10" title={t(translations.evaluator)}>
Expand All @@ -57,7 +94,23 @@ const EvaluatorFields = (props: EvaluatorFieldsProps): JSX.Element | null => {
)}
<RadioButton
className="my-0"
description={t(translations.defaultEvaluatorHint)}
description={
<>
{t(translations.defaultEvaluatorHint)}
{currentLanguage?.dependencies?.length && (
<>
<br />
{t(translations.evaluatorHasDependencies, {
viewdeps: (chunk: string): JSX.Element => (
<Link onClick={openEvaluatorDependencyPrompt}>
{chunk}
</Link>
),
})}
</>
)}
</>
}
disabled={
!currentLanguage?.whitelists.defaultEvaluator ||
props.disabled
Expand Down Expand Up @@ -189,6 +242,13 @@ const EvaluatorFields = (props: EvaluatorFieldsProps): JSX.Element | null => {
)}
/>
</Subsection>
<InstalledDependenciesPrompt
dependencies={dependencyState.dependencies}
description={dependencyState.description}
onClose={() => setDependenciesOpen(false)}
open={dependenciesOpen}
title={dependencyState.title}
/>
</>
);
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const useLanguageMode = (languages: LanguageData[]): UseLanguageModeHook => {
codaveriEvaluator: language.whitelists.codaveriEvaluator,
defaultEvaluator: language.whitelists.defaultEvaluator,
},
dependencies: language.dependencies,
};
options.push(option);
map[language.id] = language;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FC, JSXElementConstructor, ReactElement } from 'react';
import { FC, ReactElement } from 'react';
import { Link } from 'react-router-dom';
import { Chip } from '@mui/material';
import palette from 'theme/palette';
Expand Down
22 changes: 22 additions & 0 deletions client/app/bundles/course/assessment/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,28 @@ const translations = defineMessages({
defaultMessage:
'No fuss; just run the code according to the evaluation package below and report the test results.',
},
evaluatorHasDependencies: {
id: 'course.assessment.question.programming.evaluatorHasDependencies',
defaultMessage:
'This evaluator comes with <viewdeps>certain third-party dependencies installed.</viewdeps>',
},
defaultEvaluatorDependencyTitle: {
id: 'course.assessment.question.programming.defaultEvaluatorDependencyTitle',
defaultMessage: '{name}: Installed Dependencies',
},
defaultEvaluatorDependencyDescription: {
id: 'course.assessment.question.programming.defaultEvaluatorDependencyDescription',
defaultMessage:
'Submitted code is run in a containerized environment with the following dependencies installed locally.{br}If your programming question requires a dependency not listed below, <mailto>contact us</mailto> and we will consider adding it.',
},
dependencySearchText: {
id: 'course.assessment.question.programming.dependencySearchText',
defaultMessage: 'Search dependencies by name',
},
dependencyVersionTableHeading: {
id: 'course.assessment.question.programming.dependencyVersionTableHeading',
defaultMessage: 'Version',
},
codaveriEvaluator: {
id: 'course.assessment.question.programming.codaveriEvaluator',
defaultMessage: 'Codaveri',
Expand Down
2 changes: 1 addition & 1 deletion client/app/lib/components/core/buttons/RadioButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ interface RadioButtonProps {
value: string;
label: ReactNode;
className?: string;
description?: string;
description?: string | ReactNode;
disabled?: boolean;
disabledHint?: ReactNode;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ReactNode } from 'react';
import { Header, Row } from '@tanstack/react-table';
import { Row } from '@tanstack/react-table';
import { unparse } from 'papaparse';

import { ColumnTemplate, Data } from '../builder';
Expand Down
9 changes: 9 additions & 0 deletions client/app/types/course/assessment/question/programming.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import { AvailableSkills, QuestionFormData } from '../questions';

export type LanguageMode = 'c_cpp' | 'java' | 'javascript' | 'python' | 'r';

export interface LanguageDependencyData {
name: string;
version: string;
aliases?: string[];
href?: string;
title?: string;
}

export interface LanguageData {
id: number;
name: string;
Expand All @@ -11,6 +19,7 @@ export interface LanguageData {
defaultEvaluator: boolean;
codaveriEvaluator: boolean;
};
dependencies: LanguageDependencyData[];
}

export interface PackageInfoData {
Expand Down
15 changes: 15 additions & 0 deletions client/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1685,9 +1685,21 @@
"course.assessment.question.programming.defaultEvaluator": {
"defaultMessage": "Default"
},
"course.assessment.question.programming.defaultEvaluatorDependencyTitle": {
"defaultMessage": "{name}: Installed Dependencies"
},
"course.assessment.question.programming.defaultEvaluatorDependencyDescription": {
"defaultMessage": "Submitted code is run in a containerized environment with the following dependencies installed locally.{br}If your programming question requires a dependency not listed below, <mailto>contact us</mailto> and we will consider adding it."
},
"course.assessment.question.programming.defaultEvaluatorHint": {
"defaultMessage": "No fuss; just run the code according to the evaluation package below and report the test results."
},
"course.assessment.question.programming.dependencySearchText": {
"defaultMessage": "Search dependencies by name"
},
"course.assessment.question.programming.dependencyVersionTableHeading": {
"defaultMessage": "Version"
},
"course.assessment.question.programming.editOnline": {
"defaultMessage": "Create/edit online"
},
Expand Down Expand Up @@ -1724,6 +1736,9 @@
"course.assessment.question.programming.evaluator": {
"defaultMessage": "Evaluator"
},
"course.assessment.question.programming.evaluatorHasDependencies": {
"defaultMessage": "This evaluator comes with <viewdeps>certain third-party dependencies installed.</viewdeps>"
},
"course.assessment.question.programming.expected": {
"defaultMessage": "Expected"
},
Expand Down
15 changes: 15 additions & 0 deletions client/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -1700,9 +1700,21 @@
"course.assessment.question.programming.defaultEvaluator": {
"defaultMessage": "기본"
},
"course.assessment.question.programming.defaultEvaluatorDependencyTitle": {
"defaultMessage": "{name}: 설치된 종속성"
},
"course.assessment.question.programming.defaultEvaluatorDependencyDescription": {
"defaultMessage": "제출된 코드는 컨테이너화된 환경에서 실행되며, 다음 종속성이 로컬에 설치되어 있습니다.{br}프로그래밍 문제에 아래에 없는 종속성이 필요하다면, <mailto>문의해 주세요</mailto>. 추가 여부를 검토하겠습니다."
},
"course.assessment.question.programming.defaultEvaluatorHint": {
"defaultMessage": "아무 문제 없이, 아래 평가 패키지에 따라 코드를 실행하고 테스트 결과를 보고합니다."
},
"course.assessment.question.programming.dependencySearchText": {
"defaultMessage": "이름으로 종속성 검색"
},
"course.assessment.question.programming.dependencyVersionTableHeading": {
"defaultMessage": "버전"
},
"course.assessment.question.programming.editOnline": {
"defaultMessage": "온라인으로 생성/편집"
},
Expand Down Expand Up @@ -1739,6 +1751,9 @@
"course.assessment.question.programming.evaluator": {
"defaultMessage": "평가자"
},
"course.assessment.question.programming.evaluatorHasDependencies": {
"defaultMessage": "이 평가자는 <viewdeps>일부 서드파티 종속성을 설치한 상태입니다.</viewdeps>"
},
"course.assessment.question.programming.expected": {
"defaultMessage": "예상"
},
Expand Down
Loading

0 comments on commit 71d3fce

Please sign in to comment.