fix: test case with no input issue

This commit is contained in:
Jannat Patel
2025-06-24 12:22:02 +05:30
parent 88a2b69980
commit 4fb0db7a1e
12 changed files with 184 additions and 95 deletions

View File

@@ -352,7 +352,7 @@ const addProgrammingExercises = () => {
activeFor: [ activeFor: [
'ProgrammingExercises', 'ProgrammingExercises',
'ProgrammingExerciseForm', 'ProgrammingExerciseForm',
'ProgrammingExerciseSubmissionList', 'ProgrammingExerciseSubmissions',
'ProgrammingExerciseSubmission', 'ProgrammingExerciseSubmission',
], ],
}) })

View File

@@ -40,7 +40,7 @@
<template #default="{ column, item }"> <template #default="{ column, item }">
<ListRowItem :item="row[column.key]" :align="column.align"> <ListRowItem :item="row[column.key]" :align="column.align">
<div v-if="column.key == 'assessment_type'"> <div v-if="column.key == 'assessment_type'">
{{ row[column.key] == 'LMS Quiz' ? 'Quiz' : 'Assignment' }} {{ getAssessmentTypeLabel(row[column.key]) }}
</div> </div>
<div v-else-if="column.key == 'title'"> <div v-else-if="column.key == 'title'">
{{ row[column.key] }} {{ row[column.key] }}
@@ -172,6 +172,24 @@ const getRowRoute = (row) => {
}, },
} }
} }
} else if (row.assessment_type == 'LMS Programming Exercise') {
if (row.submission) {
return {
name: 'ProgrammingExerciseSubmission',
params: {
exerciseID: row.assessment_name,
submissionID: row.submission.name,
},
}
} else {
return {
name: 'ProgrammingExerciseSubmission',
params: {
exerciseID: row.assessment_name,
submissionID: 'new',
},
}
}
} else { } else {
return { return {
name: 'QuizPage', name: 'QuizPage',
@@ -221,4 +239,14 @@ const getStatusTheme = (status) => {
return 'red' return 'red'
} }
} }
const getAssessmentTypeLabel = (type) => {
if (type == 'LMS Assignment') {
return __('Assignment')
} else if (type == 'LMS Quiz') {
return __('Quiz')
} else if (type == 'LMS Programming Exercise') {
return __('Programming Exercise')
}
}
</script> </script>

View File

@@ -1,7 +1,6 @@
<template> <template>
<div> <div>
<div class="overflow-x-auto border rounded-md"> <div class="overflow-x-auto border rounded-md">
<!-- Header Row -->
<div <div
class="grid items-center space-x-4 p-2 border-b" class="grid items-center space-x-4 p-2 border-b"
:style="{ gridTemplateColumns: getGridTemplateColumns() }" :style="{ gridTemplateColumns: getGridTemplateColumns() }"
@@ -15,7 +14,7 @@
</div> </div>
<div></div> <div></div>
</div> </div>
<!-- Data Rows -->
<div <div
v-for="(row, rowIndex) in rows" v-for="(row, rowIndex) in rows"
:key="rowIndex" :key="rowIndex"
@@ -31,18 +30,20 @@
</template> </template>
<div class="relative" ref="menuRef"> <div class="relative" ref="menuRef">
<Button variant="ghost"> <Button
variant="ghost"
@click="(event: MouseEvent) => toggleMenu(rowIndex, event)"
>
<template #icon> <template #icon>
<Ellipsis <Ellipsis
class="size-4 text-ink-gray-7 stroke-1.5 cursor-pointer" class="size-4 text-ink-gray-7 stroke-1.5 cursor-pointer"
@click="toggleMenu(rowIndex)"
/> />
</template> </template>
</Button> </Button>
<div <div
v-if="menuOpenIndex === rowIndex" v-if="menuOpenIndex === rowIndex"
class="absolute right-0 z-10 mt-1 w-32 bg-white border border-outline-gray-1 rounded-md shadow-sm" class="absolute right-[30px] top-5 mt-1 w-32 bg-surface-white border border-outline-gray-1 rounded-md shadow-sm"
> >
<button <button
@click="deleteRow(rowIndex)" @click="deleteRow(rowIndex)"
@@ -58,7 +59,6 @@
</div> </div>
</div> </div>
<!-- Add Row Button -->
<div class="mt-2"> <div class="mt-2">
<Button @click="addRow"> <Button @click="addRow">
<template #prefix> <template #prefix>
@@ -77,7 +77,9 @@ import { Ellipsis, Plus, Trash2 } from 'lucide-vue-next'
import { onClickOutside } from '@vueuse/core' import { onClickOutside } from '@vueuse/core'
const rows = defineModel<Cell[][]>() const rows = defineModel<Cell[][]>()
const menuRef = ref(null)
const menuOpenIndex = ref<number | null>(null) const menuOpenIndex = ref<number | null>(null)
const menuTopPosition = ref<string>('')
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:modelValue', value: Cell[][]): void (e: 'update:modelValue', value: Cell[][]): void
}>() }>()
@@ -87,7 +89,6 @@ type Cell = {
editable?: boolean editable?: boolean
} }
// Props
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
modelValue?: Cell[][] modelValue?: Cell[][]
@@ -121,12 +122,11 @@ const getGridTemplateColumns = () => {
return [...Array(columns.value.length).fill('1fr'), '0.25fr'].join(' ') return [...Array(columns.value.length).fill('1fr'), '0.25fr'].join(' ')
} }
const toggleMenu = (index: number) => { const toggleMenu = (index: number, event: MouseEvent) => {
menuOpenIndex.value = menuOpenIndex.value === index ? null : index menuOpenIndex.value = menuOpenIndex.value === index ? null : index
menuTopPosition.value = `${event.clientY + 10}px`
} }
// Optional: Close menu when clicking outside
const menuRef = ref(null)
onClickOutside(menuRef, () => { onClickOutside(menuRef, () => {
menuOpenIndex.value = null menuOpenIndex.value = null
}) })

View File

@@ -99,6 +99,7 @@ const assessmentTypes = computed(() => {
return [ return [
{ label: 'Quiz', value: 'LMS Quiz' }, { label: 'Quiz', value: 'LMS Quiz' },
{ label: 'Assignment', value: 'LMS Assignment' }, { label: 'Assignment', value: 'LMS Assignment' },
{ label: 'Programming Exercise', value: 'LMS Programming Exercise' },
] ]
}) })
</script> </script>

View File

@@ -1,5 +1,5 @@
<template> <template>
<Dialog v-model="show" :options="{ size: '2xl' }"> <Dialog v-model="show" :options="{ size: '5xl' }">
<template #body-title> <template #body-title>
<div class="text-xl font-semibold text-ink-gray-9"> <div class="text-xl font-semibold text-ink-gray-9">
{{ {{
@@ -10,41 +10,45 @@
</div> </div>
</template> </template>
<template #body-content> <template #body-content>
<div class="space-y-4"> <div class="grid grid-cols-2 gap-10">
<FormControl <div class="space-y-4">
v-model="exercise.title" <FormControl
:label="__('Title')" v-model="exercise.title"
:required="true" :label="__('Title')"
/> :required="true"
<FormControl />
v-model="exercise.language" <FormControl
:label="__('Language')" v-model="exercise.language"
type="select" :label="__('Language')"
:options="languageOptions" type="select"
:required="true" :options="languageOptions"
/> :required="true"
<div> />
<div class="text-xs text-ink-gray-5 mb-2"> <ChildTable
{{ __('Problem Statement') }} v-model="exercise.test_cases"
<span class="text-ink-red-3">*</span> :columns="testCaseColumns"
</div> :required="true"
<TextEditor :addable="true"
:content="exercise.problem_statement" :deletable="true"
@change="(val: string) => (exercise.problem_statement = val)"
:editable="true" :editable="true"
:fixedMenu="true" :placeholder="__('Add Test Case')"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[18rem] overflow-y-auto"
/> />
</div> </div>
<ChildTable <div>
v-model="exercise.test_cases" <div>
:columns="testCaseColumns" <div class="text-xs text-ink-gray-5 mb-2">
:required="true" {{ __('Problem Statement') }}
:addable="true" <span class="text-ink-red-3">*</span>
:deletable="true" </div>
:editable="true" <TextEditor
:placeholder="__('Add Test Case')" :content="exercise.problem_statement"
/> @change="(val: string) => (exercise.problem_statement = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] max-h-[21rem] overflow-y-auto"
/>
</div>
</div>
</div> </div>
</template> </template>
<template #actions="{ close }"> <template #actions="{ close }">
@@ -65,7 +69,9 @@
<router-link <router-link
:to="{ :to="{
name: 'ProgrammingExerciseSubmissions', name: 'ProgrammingExerciseSubmissions',
params: { exerciseID: props.exerciseID }, query: {
exercise: props.exerciseID,
},
}" }"
> >
<Button> <Button>
@@ -105,12 +111,14 @@ const exercises = defineModel<{
) => void ) => void
} }
}>('exercises') }>('exercises')
const exercise = ref<ProgrammingExercise>({ const exercise = ref<ProgrammingExercise>({
title: '', title: '',
language: 'Python', language: 'Python',
problem_statement: '', problem_statement: '',
test_cases: [], test_cases: [],
}) })
const languageOptions = [ const languageOptions = [
{ label: 'Python', value: 'Python' }, { label: 'Python', value: 'Python' },
{ label: 'JavaScript', value: 'JavaScript' }, { label: 'JavaScript', value: 'JavaScript' },
@@ -159,6 +167,8 @@ const fetchTestCases = () => {
testCases.update({ testCases.update({
filters: { filters: {
parent: props.exerciseID, parent: props.exerciseID,
parenttype: 'LMS Programming Exercise',
parentfield: 'test_cases',
}, },
}) })
testCases.reload() testCases.reload()

View File

@@ -11,7 +11,7 @@
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal border-r px-5 py-2 h-full" class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal border-r px-5 py-2 h-full"
></div> ></div>
<div> <div>
<div class="flex items-center justify-between p-2 bg-surface-gray-1"> <div class="flex items-center justify-between p-2 bg-surface-gray-2">
<div class="font-semibold"> <div class="font-semibold">
{{ exercise.doc?.language }} {{ exercise.doc?.language }}
</div> </div>
@@ -38,7 +38,7 @@
maxHeight="1000px" maxHeight="1000px"
/> />
<span v-if="error" class="text-xs text-ink-gray-5 px-2"> <span v-if="error" class="text-xs text-ink-gray-5 px-2">
{{ __('Compiler Error') }}: {{ __('Compiler Message') }}:
</span> </span>
<textarea <textarea
v-if="error" v-if="error"
@@ -48,6 +48,7 @@
/> />
<!-- <textarea v-else v-model="output" class="bg-surface-gray-1 border-none text-sm h-28 leading-6" readonly /> --> <!-- <textarea v-else v-model="output" class="bg-surface-gray-1 border-none text-sm h-28 leading-6" readonly /> -->
</div> </div>
<div ref="testCaseSection" v-if="testCases.length" class="p-3"> <div ref="testCaseSection" v-if="testCases.length" class="p-3">
<span class="text-lg font-semibold text-ink-gray-9"> <span class="text-lg font-semibold text-ink-gray-9">
{{ __('Test Cases') }} {{ __('Test Cases') }}
@@ -132,7 +133,7 @@ const testCases = ref<
}> }>
>([]) >([])
const boilerplate = ref<string>( const boilerplate = ref<string>(
`with open("stdin", "r") as f:\n data = f.read()\n\ninputs = data.split()\n\n# inputs is a list of strings\n# write your code below\n\n` `with open("stdin", "r") as f:\n data = f.read()\n\ninputs = data.split() if len(data) else []\n\n# inputs is a list of strings\n# write your code below\n\n`
) )
const { brand } = sessionStore() const { brand } = sessionStore()
const router = useRouter() const router = useRouter()
@@ -179,7 +180,9 @@ watch(
(doc) => { (doc) => {
if (doc) { if (doc) {
code.value = `${boilerplate.value}${doc.code || ''}\n` code.value = `${boilerplate.value}${doc.code || ''}\n`
testCases.value = doc.test_cases || [] if (testCases.value.length === 0) {
testCases.value = doc.test_cases || []
}
} }
}, },
{ immediate: true } { immediate: true }
@@ -207,6 +210,7 @@ const runCode = async () => {
if (testCaseSection.value) { if (testCaseSection.value) {
testCaseSection.value.scrollIntoView({ behavior: 'smooth' }) testCaseSection.value.scrollIntoView({ behavior: 'smooth' })
} }
for (const test_case of exercise.doc.test_cases) { for (const test_case of exercise.doc.test_cases) {
let result = await execute(test_case.input) let result = await execute(test_case.input)
if (error.value) { if (error.value) {
@@ -217,7 +221,6 @@ const runCode = async () => {
} }
let status = let status =
result.trim() === test_case.expected_output.trim() ? 'Passed' : 'Failed' result.trim() === test_case.expected_output.trim() ? 'Passed' : 'Failed'
testCases.value.push({ testCases.value.push({
input: test_case.input, input: test_case.input,
output: result, output: result,
@@ -256,41 +259,57 @@ const createSubmission = () => {
}) })
} }
const execute = async (stdin = '') => { const execute = (stdin = ''): Promise<string> => {
return new Promise<string>((resolve, reject) => { return new Promise((resolve, reject) => {
let outputChunks: string[] = [] let outputChunks: string[] = []
let hasExited = false
let hasError = false
const session = new LiveCodeSession({ let session = new LiveCodeSession({
base_url: 'https://falcon.frappe.io', base_url: 'https://falcon.frappe.io',
runtime: 'python', runtime: 'python',
code: code.value, code: code.value,
files: [{ filename: 'stdin', contents: stdin }], files: [{ filename: 'stdin', contents: stdin }],
onMessage: (msg: any) => { onMessage: (msg: any) => {
console.log('msg', msg)
if (msg.msgtype === 'write' && msg.file === 'stdout') { if (msg.msgtype === 'write' && msg.file === 'stdout') {
outputChunks.push(msg.data) outputChunks.push(msg.data)
} }
if (msg.msgtype == 'exitstatus') { if (msg.msgtype === 'write' && msg.file === 'stderr') {
if (msg.exitstatus != 0) { hasError = true
errorMessage.value = msg.data
}
if (msg.msgtype === 'exitstatus') {
hasExited = true
if (msg.exitstatus !== 0) {
error.value = true error.value = true
} else { } else {
error.value = false error.value = false
} }
} resolve(outputChunks.join('').trim())
if (msg.msgtype === 'exitstatus') {
resolve(outputChunks.join(''))
} }
}, },
}) })
setTimeout(() => reject('Execution timed out.'), 10000) setTimeout(() => {
if (!hasExited) {
error.value = true
errorMessage.value = 'Execution timed out.'
reject('Execution timed out.')
}
}, 20000)
}) })
} }
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
return [ return [
{ label: __('Programming Exercises') }, {
label: __('Programming Exercise Submissions'),
route: { name: 'ProgrammingExerciseSubmissions' },
},
{ label: exercise.doc?.title }, { label: exercise.doc?.title },
] ]
}) })

View File

@@ -129,18 +129,20 @@ import {
ListRowItem, ListRowItem,
usePageMeta, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, ref, watch } from 'vue'
import { sessionStore } from '@/stores/session'
import type { import type {
ProgrammingExerciseSubmission, ProgrammingExerciseSubmission,
Filters, Filters,
} from '@/pages/ProgrammingExercises/types' } from '@/pages/ProgrammingExercises/types'
import { computed, inject, onMounted, ref, watch } from 'vue'
import { sessionStore } from '@/stores/session'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import EmptyState from '@/components/EmptyState.vue' import EmptyState from '@/components/EmptyState.vue'
const { brand } = sessionStore() const { brand } = sessionStore()
const dayjs = inject('$dayjs') as any const dayjs = inject('$dayjs') as any
const user = inject('$user') as any
const filterFields = ['exercise', 'member', 'status']
const filters = ref<Filters>({ const filters = ref<Filters>({
exercise: '', exercise: '',
member: '', member: '',
@@ -148,6 +150,19 @@ const filters = ref<Filters>({
}) })
const router = useRouter() const router = useRouter()
onMounted(() => {
if (!user.data?.is_instructor && !user.data?.is_moderator) {
router.push({ name: 'Courses' })
}
filterFields.forEach((field) => {
if (router.currentRoute.value.query[field]) {
filters.value[field as keyof Filters] = router.currentRoute.value.query[
field
] as string
}
})
})
const submissions = createListResource({ const submissions = createListResource({
doctype: 'LMS Programming Exercise Submission', doctype: 'LMS Programming Exercise Submission',
fields: [ fields: [
@@ -172,7 +187,6 @@ const submissions = createListResource({
watch(filters.value, () => { watch(filters.value, () => {
let filtersToApply: Record<string, any> = {} let filtersToApply: Record<string, any> = {}
const filterFields = ['exercise', 'member', 'status']
filterFields.forEach((field) => { filterFields.forEach((field) => {
if (filters.value[field as keyof Filters]) { if (filters.value[field as keyof Filters]) {
filtersToApply[field] = filters.value[field as keyof Filters] filtersToApply[field] = filters.value[field as keyof Filters]
@@ -211,7 +225,7 @@ const submissionColumns = computed(() => {
{ {
label: __('Exercise'), label: __('Exercise'),
key: 'exercise_title', key: 'exercise_title',
width: '40%', width: '30%',
icon: 'code', icon: 'code',
}, },
{ {
@@ -225,6 +239,7 @@ const submissionColumns = computed(() => {
key: 'modified', key: 'modified',
width: '20%', width: '20%',
icon: 'clock', icon: 'clock',
align: 'right',
}, },
] ]
}) })

View File

@@ -54,7 +54,7 @@
showForm = true showForm = true
} }
" "
class="flex flex-col border rounded-md p-3 h-full hover:border-outline-gray-3 space-y-2" class="flex flex-col border rounded-md p-3 h-full hover:border-outline-gray-3 space-y-2 cursor-pointer"
> >
<div class="text-lg font-semibold text-ink-gray-9"> <div class="text-lg font-semibold text-ink-gray-9">
{{ exercise.title }} {{ exercise.title }}

View File

@@ -216,20 +216,13 @@ const routes = [
component: () => import('@/pages/PersonaForm.vue'), component: () => import('@/pages/PersonaForm.vue'),
}, },
{ {
path: '/exercises', path: '/programming-exercises',
name: 'ProgrammingExercises', name: 'ProgrammingExercises',
component: () => component: () =>
import('@/pages/ProgrammingExercises/ProgrammingExercises.vue'), import('@/pages/ProgrammingExercises/ProgrammingExercises.vue'),
}, },
{ {
path: '/exercises/:exerciseID', path: '/programming-exercises/submissions',
name: 'ProgrammingExerciseForm',
component: () =>
import('@/pages/ProgrammingExercises/ProgrammingExerciseForm.vue'),
props: true,
},
{
path: '/exercises/submissions',
name: 'ProgrammingExerciseSubmissions', name: 'ProgrammingExerciseSubmissions',
component: () => component: () =>
import( import(
@@ -238,7 +231,7 @@ const routes = [
props: true, props: true,
}, },
{ {
path: '/exercises/:exerciseID/submission/:submissionID', path: '/programming-exercises/:exerciseID/submission/:submissionID',
name: 'ProgrammingExerciseSubmission', name: 'ProgrammingExerciseSubmission',
component: () => component: () =>
import( import(

View File

@@ -1544,24 +1544,18 @@ def get_exercise_status(test_cases):
def update_test_cases(test_cases, submission): def update_test_cases(test_cases, submission):
frappe.db.delete("LMS Test Case Submission", {"parent": submission})
for row in test_cases: for row in test_cases:
test_case = frappe.db.get_value( test_case = frappe.new_doc("LMS Test Case Submission")
"LMS Test Case Submission", test_case.update(
{ {
"parent": submission, "parent": submission,
"parenttype": "LMS Programming Exercise Submission",
"parentfield": "test_cases",
"input": row.get("input"), "input": row.get("input"),
}, "output": row.get("output"),
"name", "expected_output": row.get("expected_output"),
as_dict=True, "status": row.get("status", "Failed"),
}
) )
test_case.insert()
if test_case:
frappe.db.set_value(
"LMS Test Case Submission",
test_case.name,
{
"output": row.get("output"),
"expected_output": row.get("expected_output"),
"status": row.get("status", "Failed"),
},
)

View File

@@ -16,6 +16,7 @@
{ {
"fieldname": "input", "fieldname": "input",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1,
"label": "Input" "label": "Input"
}, },
{ {
@@ -49,8 +50,8 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2025-06-18 20:06:57.182153", "modified": "2025-06-24 11:23:13.803159",
"modified_by": "Administrator", "modified_by": "sayali@frappe.io",
"module": "LMS", "module": "LMS",
"name": "LMS Test Case Submission", "name": "LMS Test Case Submission",
"owner": "Administrator", "owner": "Administrator",

View File

@@ -1504,6 +1504,9 @@ def get_assessments(batch, member=None):
elif assessment.assessment_type == "LMS Quiz": elif assessment.assessment_type == "LMS Quiz":
assessment = get_quiz_details(assessment, member) assessment = get_quiz_details(assessment, member)
elif assessment.assessment_type == "LMS Programming Exercise":
assessment = get_exercise_details(assessment, member)
return assessments return assessments
@@ -1576,6 +1579,31 @@ def get_quiz_details(assessment, member):
return assessment return assessment
def get_exercise_details(assessment, member):
assessment.title = frappe.db.get_value(
"LMS Programming Exercise", assessment.assessment_name, "title"
)
filters = {"member": member, "exercise": assessment.assessment_name}
if frappe.db.exists("LMS Programming Exercise Submission", filters):
assessment.submission = frappe.db.get_value(
"LMS Programming Exercise Submission",
filters,
["name", "status"],
as_dict=True,
)
assessment.completed = True
assessment.status = assessment.submission.status
assessment.edit_url = (
f"/exercises/{assessment.assessment_name}/submission/{assessment.submission.name}"
)
else:
assessment.status = "Not Attempted"
assessment.color = "red"
assessment.completed = False
assessment.edit_url = f"/exercises/{assessment.assessment_name}/submission/new"
@frappe.whitelist() @frappe.whitelist()
def get_batch_students(batch): def get_batch_students(batch):
students = [] students = []