Skip to content

Commit 6038124

Browse files
author
adi-herwana-nus
committed
feat(question-generation): support Java question generation
1 parent 54de808 commit 6038124

15 files changed

+187
-92
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,33 @@
11
# frozen_string_literal: true
22
module CodaveriLanguageConcern
3-
# TODO: Codaveri currently only accepts major.minor versions, so ".0" minor version is added for Java
4-
# When Codaveri supports a graceful fallback to specific major.minor.patch when only major version is specified,
5-
# we should remove this concern
3+
def codaveri_language
4+
programming_language_map[type.constantize]&.fetch(:language) || polyglot_name
5+
end
6+
7+
def codaveri_version
8+
programming_language_map[type.constantize]&.fetch(:version) || polyglot_version
9+
end
10+
11+
private
612

7-
def polyglot_version
8-
(polyglot_name == 'java') ? "#{super}.0" : super
13+
def programming_language_map
14+
{
15+
Coursemology::Polyglot::Language::CPlusPlus::CPlusPlus11 => {
16+
language: 'cpp',
17+
version: '10.2'
18+
},
19+
Coursemology::Polyglot::Language::CPlusPlus::CPlusPlus17 => {
20+
language: 'cpp',
21+
version: '10.2'
22+
},
23+
Coursemology::Polyglot::Language::Java::Java17 => {
24+
language: 'java',
25+
version: '17.0'
26+
},
27+
Coursemology::Polyglot::Language::Java::Java21 => {
28+
language: 'java',
29+
version: '21.0'
30+
}
31+
}
932
end
1033
end

app/controllers/course/assessment/question/programming_controller.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ def generate
8787
generation_service = Course::Assessment::Question::CodaveriProblemGenerationService.new(
8888
@assessment,
8989
params,
90-
language.polyglot_name,
91-
language.extend(CodaveriLanguageConcern).polyglot_version
90+
language.extend(CodaveriLanguageConcern).codaveri_language,
91+
language.extend(CodaveriLanguageConcern).codaveri_version
9292
)
9393

9494
generated_problem = generation_service.codaveri_generate_problem

app/services/course/assessment/answer/programming_codaveri_async_feedback_service.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ def construct_feedback_object
7575
@answer_object[:problemId] = @question.codaveri_id
7676

7777
@answer_object[:languageVersion] = {
78-
language: @question.language.polyglot_name,
79-
version: @question.language.extend(CodaveriLanguageConcern).polyglot_version
78+
language: @question.language.extend(CodaveriLanguageConcern).codaveri_language,
79+
version: @question.language.extend(CodaveriLanguageConcern).codaveri_version
8080
}
8181

8282
@answer_files.each do |file|

app/services/course/assessment/answer/programming_codaveri_feedback_service.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,10 @@ def construct_feedback_object # rubocop:disable Metrics/AbcSize
3535

3636
@answer_object[:problem_id] = @question.codaveri_id
3737

38-
@answer_object[:language_version][:language] = @question.language.polyglot_name
38+
@answer_object[:language_version][:language] =
39+
@question.language.extend(CodaveriLanguageConcern).codaveri_language
3940
@answer_object[:language_version][:version] =
40-
@question.language.extend(CodaveriLanguageConcern).polyglot_version
41+
@question.language.extend(CodaveriLanguageConcern).codaveri_version
4142

4243
@answer_object[:is_only_itsp] = true if @course.codaveri_itsp_enabled?
4344

app/services/course/assessment/programming_codaveri_evaluation_service.rb

+2-2
Original file line numberDiff line numberDiff line change
@@ -155,8 +155,8 @@ def construct_grading_object
155155

156156
@answer_object[:problemId] = @question.codaveri_id
157157

158-
@answer_object[:languageVersion][:language] = @question.language.polyglot_name
159-
@answer_object[:languageVersion][:version] = @question.language.extend(CodaveriLanguageConcern).polyglot_version
158+
@answer_object[:languageVersion][:language] = @question.language.extend(CodaveriLanguageConcern).codaveri_language
159+
@answer_object[:languageVersion][:version] = @question.language.extend(CodaveriLanguageConcern).codaveri_version
160160

161161
@answer.files.each do |file|
162162
file_template = default_codaveri_student_file_template

app/services/course/assessment/question/codaveri_problem_generation_service.rb

+12-3
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,18 @@ def initialize(assessment, params, language, version)
4747

4848
return unless params[:is_default_question_form_data] == 'false'
4949

50-
# TODO: update the path to support other languages should it become available next time
5150
@payload = @payload.merge({
5251
problem: {
5352
title: params[:title],
5453
description: params[:description],
5554
templates: [{
56-
path: 'main.py',
55+
path: generate_payload_file_name(language, params[:template]),
5756
content: params[:template]
5857
}],
5958
solutions: [{
6059
tag: 'solution',
6160
files: [{
62-
path: 'main.py',
61+
path: generate_payload_file_name(language, params[:solution]),
6362
content: params[:solution]
6463
}]
6564
}],
@@ -70,6 +69,15 @@ def initialize(assessment, params, language, version)
7069
append_test_cases_to_problem_payload('public', params[:public_test_cases])
7170
append_test_cases_to_problem_payload('private', params[:private_test_cases])
7271
append_test_cases_to_problem_payload('hidden', params[:evaluation_test_cases])
72+
73+
p({ generate_payload: @payload })
74+
end
75+
76+
def generate_payload_file_name(codaveri_language, file_content)
77+
return 'main.py' if codaveri_language == 'python'
78+
79+
match = file_content.match(/\bclass\s+(\w+)\s*\{/)
80+
match ? "#{match[1]}.java" : 'Main.java'
7381
end
7482

7583
def send_problem_generation_request
@@ -103,6 +111,7 @@ def append_test_cases_to_problem_payload(visibility, test_cases)
103111
index: @payload[:problem][:exprTestcases].length + 1,
104112
visibility: visibility,
105113
hint: test_case['hint'],
114+
prefix: test_case['inlineCode'] || '',
106115
lhsExpression: test_case['expression'],
107116
rhsExpression: test_case['expected'],
108117
display: test_case['expression']

app/services/course/assessment/question/programming_codaveri_service.rb

+3-2
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,10 @@ def construct_problem_object(package) # rubocop:disable Metrics/AbcSize
7777
@problem_object[:title] = @question.title
7878
@problem_object[:description] = @question.description
7979
resources_object = @problem_object[:resources][0]
80-
resources_object[:languageVersions][:language] = @question.language.polyglot_name
80+
resources_object[:languageVersions][:language] =
81+
@question.language.extend(CodaveriLanguageConcern).codaveri_language
8182
resources_object[:languageVersions][:versions] =
82-
[@question.language.extend(CodaveriLanguageConcern).polyglot_version]
83+
[@question.language.extend(CodaveriLanguageConcern).codaveri_version]
8384

8485
codaveri_package = Course::Assessment::Question::ProgrammingCodaveri::ProgrammingCodaveriPackageService.new(
8586
@question, package

client/app/bundles/course/assessment/pages/AssessmentGenerate/GenerateProgrammingQuestionPage.tsx

+5-5
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,9 @@ const GenerateProgrammingQuestionPage = (): JSX.Element => {
255255
return (
256256
<Preload render={<LoadingIndicator />} while={fetchCodaveriLanguages}>
257257
{(languages): JSX.Element => {
258+
const currentLanguageMode =
259+
languages.find((language) => language.id === currentLanguageId)
260+
?.editorMode ?? 'python';
258261
return (
259262
<>
260263
<GenerateTabs
@@ -349,6 +352,7 @@ const GenerateProgrammingQuestionPage = (): JSX.Element => {
349352
buildGenerateRequestPayload(
350353
codaveriFormData,
351354
questionFormData,
355+
currentLanguageMode === 'java',
352356
),
353357
)
354358
.then((response) => {
@@ -477,11 +481,7 @@ const GenerateProgrammingQuestionPage = (): JSX.Element => {
477481

478482
<Grid item lg={8} xs={12}>
479483
<GenerateQuestionPrototypeForm
480-
editorMode={
481-
languages.find(
482-
(language) => language.id === currentLanguageId,
483-
)?.editorMode ?? 'python'
484-
}
484+
editorMode={currentLanguageMode}
485485
lockStates={lockStates}
486486
onToggleLock={(lockStateKey: string) => {
487487
setLockStates({

client/app/bundles/course/assessment/pages/AssessmentGenerate/GenerateQuestionPrototypeForm.tsx

+25-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1-
import { FC } from 'react';
1+
import { ElementType, FC } from 'react';
22
import { Controller, FormProvider, UseFormReturn } from 'react-hook-form';
33
import { Container } from '@mui/material';
44
import { LanguageMode } from 'types/course/assessment/question/programming';
55

66
import EditorAccordion from 'course/assessment/question/programming/components/common/EditorAccordion';
7+
import ReorderableJavaTestCase from 'course/assessment/question/programming/components/common/ReorderableJavaTestCase';
8+
import ReorderableTestCase, {
9+
ReorderableTestCaseProps,
10+
} from 'course/assessment/question/programming/components/common/ReorderableTestCase';
711
import { generationActions as actions } from 'course/assessment/reducers/generation';
812
import FormRichTextField from 'lib/components/form/fields/RichTextField';
913
import FormTextField from 'lib/components/form/fields/TextField';
@@ -23,6 +27,17 @@ interface Props {
2327
editorMode: LanguageMode;
2428
}
2529

30+
const TestCaseComponentMapper: Record<
31+
LanguageMode,
32+
ElementType<ReorderableTestCaseProps>
33+
> = {
34+
python: ReorderableTestCase,
35+
java: ReorderableJavaTestCase,
36+
c_cpp: ReorderableTestCase,
37+
javascript: ReorderableTestCase,
38+
r: ReorderableTestCase,
39+
};
40+
2641
const GenerateQuestionPrototypeForm: FC<Props> = (props) => {
2742
const { prototypeForm, lockStates, onToggleLock, editorMode } = props;
2843
const { t } = useTranslation();
@@ -34,6 +49,8 @@ const GenerateQuestionPrototypeForm: FC<Props> = (props) => {
3449
},
3550
});
3651

52+
const TestCaseComponent = TestCaseComponentMapper[editorMode];
53+
3754
return (
3855
<FormProvider {...prototypeForm}>
3956
<LockableSection
@@ -133,7 +150,13 @@ const GenerateQuestionPrototypeForm: FC<Props> = (props) => {
133150
</Container>
134151
</LockableSection>
135152

136-
<TestCasesManager lockStates={lockStates} onToggleLock={onToggleLock} />
153+
<TestCasesManager
154+
component={TestCaseComponent}
155+
control={prototypeForm.control}
156+
lockStates={lockStates}
157+
onToggleLock={onToggleLock}
158+
setValue={prototypeForm.setValue}
159+
/>
137160
</FormProvider>
138161
);
139162
};
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { FC, ReactNode } from 'react';
2+
import { defineMessages } from 'react-intl';
23
import { LockOpenOutlined, LockOutlined } from '@mui/icons-material';
3-
import { Divider, IconButton } from '@mui/material';
4+
import { Divider, IconButton, Tooltip } from '@mui/material';
5+
6+
import useTranslation from 'lib/hooks/useTranslation';
47

58
interface LockableSectionProps {
69
onToggleLock: (key: string) => void;
@@ -9,20 +12,44 @@ interface LockableSectionProps {
912
lockState: boolean;
1013
}
1114

12-
const LockableSection: FC<LockableSectionProps> = (props) => (
13-
<>
14-
<div className="flex flex-nowrap">
15-
<IconButton
16-
centerRipple={false}
17-
className="m-1 rounded-lg items-start"
18-
onClick={() => props.onToggleLock(props.lockStateKey)}
19-
>
20-
{props.lockState ? <LockOutlined /> : <LockOpenOutlined />}
21-
</IconButton>
22-
{props.children}
23-
</div>
24-
<Divider className="my-4" variant="middle" />
25-
</>
26-
);
15+
const translations = defineMessages({
16+
lockTooltip: {
17+
id: 'course.assessment.generation.lockTooltip',
18+
defaultMessage:
19+
'Lock this section if you do not want it changed by generation',
20+
},
21+
unlockTooltip: {
22+
id: 'course.assessment.generation.unlockTooltip',
23+
defaultMessage: 'Unlock this section to continue editing it',
24+
},
25+
});
26+
27+
const LockableSection: FC<LockableSectionProps> = (props) => {
28+
const { t } = useTranslation();
29+
return (
30+
<>
31+
<div className="flex flex-nowrap">
32+
<IconButton
33+
centerRipple={false}
34+
className="m-1 rounded-lg items-start"
35+
onClick={() => props.onToggleLock(props.lockStateKey)}
36+
>
37+
<Tooltip
38+
placement="top-start"
39+
title={
40+
props.lockState
41+
? t(translations.unlockTooltip)
42+
: t(translations.lockTooltip)
43+
}
44+
>
45+
{props.lockState ? <LockOutlined /> : <LockOpenOutlined />}
46+
</Tooltip>
47+
</IconButton>
48+
{props.children}
49+
</div>
50+
<Divider className="my-4" variant="middle" />
51+
</>
52+
);
53+
};
2754

2855
export default LockableSection;

client/app/bundles/course/assessment/pages/AssessmentGenerate/TestCasesManager.tsx

+16-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
import { FC } from 'react';
2-
import { useFormContext, useWatch } from 'react-hook-form';
1+
import { ElementType, FC } from 'react';
2+
import { Control, UseFormSetValue, useWatch } from 'react-hook-form';
33
import { DragDropContext, DropResult } from '@hello-pangea/dnd';
44
import { Container } from '@mui/material';
55
import { ProgrammingFormData } from 'types/course/assessment/question/programming';
66

7+
import { ReorderableTestCaseProps } from 'course/assessment/question/programming/components/common/ReorderableTestCase';
78
import ReorderableTestCases from 'course/assessment/question/programming/components/common/ReorderableTestCases';
89
import {
910
deleteTestCase,
@@ -14,17 +15,25 @@ import useTranslation from 'lib/hooks/useTranslation';
1415
import translations from '../../translations';
1516

1617
import LockableSection from './LockableSection';
18+
import { QuestionPrototypeFormData } from './types';
1719

1820
interface TestCasesManagerProps {
21+
control: Control<QuestionPrototypeFormData>;
22+
setValue: UseFormSetValue<QuestionPrototypeFormData>;
1923
lockStates: { [key: string]: boolean };
2024
onToggleLock: (key: string) => void;
25+
component?: ElementType<ReorderableTestCaseProps>;
2126
}
2227

2328
const TestCasesManager: FC<TestCasesManagerProps> = (props) => {
2429
const { t } = useTranslation();
25-
const { lockStates, onToggleLock } = props;
30+
const { component, lockStates, onToggleLock } = props;
31+
32+
// Cast fields to ProgrammingFormData to satisfy helper components' type assertions
33+
const control = props.control as unknown as Control<ProgrammingFormData>;
34+
const setValue =
35+
props.setValue as unknown as UseFormSetValue<ProgrammingFormData>;
2636

27-
const { control, setValue } = useFormContext<ProgrammingFormData>();
2837
const testCases = useWatch({ control, name: 'testUi.metadata.testCases' });
2938

3039
const onRearrangingTestCases = (result: DropResult): void => {
@@ -48,6 +57,7 @@ const TestCasesManager: FC<TestCasesManagerProps> = (props) => {
4857
>
4958
<Container disableGutters maxWidth={false}>
5059
<ReorderableTestCases
60+
component={component}
5161
control={control}
5262
disabled={lockStates[publicTestCasesName]}
5363
hintHeader={t(translations.hint)}
@@ -70,6 +80,7 @@ const TestCasesManager: FC<TestCasesManagerProps> = (props) => {
7080
>
7181
<Container disableGutters maxWidth={false}>
7282
<ReorderableTestCases
83+
component={component}
7384
control={control}
7485
disabled={lockStates[privateTestCasesName]}
7586
hintHeader={t(translations.hint)}
@@ -93,6 +104,7 @@ const TestCasesManager: FC<TestCasesManagerProps> = (props) => {
93104
>
94105
<Container disableGutters maxWidth={false}>
95106
<ReorderableTestCases
107+
component={component}
96108
control={control}
97109
disabled={lockStates[evaluationTestCasesName]}
98110
hintHeader={t(translations.hint)}

client/app/bundles/course/assessment/pages/AssessmentGenerate/constants.ts

+3-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import { MetadataTestCase } from 'types/course/assessment/question/programming';
2-
31
import { CodaveriGenerateFormData, QuestionPrototypeFormData } from './types';
42

53
export const defaultQuestionFormData: QuestionPrototypeFormData = {
@@ -12,9 +10,9 @@ export const defaultQuestionFormData: QuestionPrototypeFormData = {
1210
solution: '',
1311
submission: '',
1412
testCases: {
15-
public: [] as MetadataTestCase[],
16-
private: [] as MetadataTestCase[],
17-
evaluation: [] as MetadataTestCase[],
13+
public: [],
14+
private: [],
15+
evaluation: [],
1816
},
1917
},
2018
},

0 commit comments

Comments
 (0)