feat: javascript exercises
This commit is contained in:
1
frontend/components.d.ts
vendored
1
frontend/components.d.ts
vendored
@@ -79,7 +79,6 @@ declare module 'vue' {
|
|||||||
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
|
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']
|
||||||
PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default']
|
PaymentSettings: typeof import('./src/components/Settings/PaymentSettings.vue')['default']
|
||||||
Play: typeof import('./src/components/Icons/Play.vue')['default']
|
Play: typeof import('./src/components/Icons/Play.vue')['default']
|
||||||
ProgrammingExerciseModal: typeof import('./src/components/Modals/ProgrammingExerciseModal.vue')['default']
|
|
||||||
ProgressBar: typeof import('./src/components/ProgressBar.vue')['default']
|
ProgressBar: typeof import('./src/components/ProgressBar.vue')['default']
|
||||||
Question: typeof import('./src/components/Modals/Question.vue')['default']
|
Question: typeof import('./src/components/Modals/Question.vue')['default']
|
||||||
Quiz: typeof import('./src/components/Quiz.vue')['default']
|
Quiz: typeof import('./src/components/Quiz.vue')['default']
|
||||||
|
|||||||
@@ -313,7 +313,7 @@ const addNotifications = () => {
|
|||||||
|
|
||||||
const addQuizzes = () => {
|
const addQuizzes = () => {
|
||||||
if (isInstructor.value || isModerator.value) {
|
if (isInstructor.value || isModerator.value) {
|
||||||
sidebarLinks.value.push({
|
sidebarLinks.value.splice(4, 0, {
|
||||||
label: 'Quizzes',
|
label: 'Quizzes',
|
||||||
icon: 'CircleHelp',
|
icon: 'CircleHelp',
|
||||||
to: 'Quizzes',
|
to: 'Quizzes',
|
||||||
@@ -329,7 +329,7 @@ const addQuizzes = () => {
|
|||||||
|
|
||||||
const addAssignments = () => {
|
const addAssignments = () => {
|
||||||
if (isInstructor.value || isModerator.value) {
|
if (isInstructor.value || isModerator.value) {
|
||||||
sidebarLinks.value.push({
|
sidebarLinks.value.splice(5, 0, {
|
||||||
label: 'Assignments',
|
label: 'Assignments',
|
||||||
icon: 'Pencil',
|
icon: 'Pencil',
|
||||||
to: 'Assignments',
|
to: 'Assignments',
|
||||||
@@ -343,22 +343,6 @@ const addAssignments = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addProgrammingExercises = () => {
|
|
||||||
if (isInstructor.value || isModerator.value) {
|
|
||||||
sidebarLinks.value.push({
|
|
||||||
label: 'Programming Exercises',
|
|
||||||
icon: 'Code',
|
|
||||||
to: 'ProgrammingExercises',
|
|
||||||
activeFor: [
|
|
||||||
'ProgrammingExercises',
|
|
||||||
'ProgrammingExerciseForm',
|
|
||||||
'ProgrammingExerciseSubmissions',
|
|
||||||
'ProgrammingExerciseSubmission',
|
|
||||||
],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const addPrograms = () => {
|
const addPrograms = () => {
|
||||||
let activeFor = ['Programs', 'ProgramForm']
|
let activeFor = ['Programs', 'ProgramForm']
|
||||||
let index = 1
|
let index = 1
|
||||||
@@ -644,7 +628,6 @@ watch(userResource, () => {
|
|||||||
addPrograms()
|
addPrograms()
|
||||||
addQuizzes()
|
addQuizzes()
|
||||||
addAssignments()
|
addAssignments()
|
||||||
addProgrammingExercises()
|
|
||||||
setUpOnboarding()
|
setUpOnboarding()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -231,7 +231,7 @@ const getAssessmentColumns = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getStatusTheme = (status) => {
|
const getStatusTheme = (status) => {
|
||||||
if (status === 'Pass') {
|
if (status === 'Pass' || status === 'Passed') {
|
||||||
return 'green'
|
return 'green'
|
||||||
} else if (status === 'Not Graded') {
|
} else if (status === 'Not Graded') {
|
||||||
return 'orange'
|
return 'orange'
|
||||||
|
|||||||
@@ -6,13 +6,12 @@
|
|||||||
:courses="batch.data.courses"
|
:courses="batch.data.courses"
|
||||||
/>
|
/>
|
||||||
<Assessments :batch="batch.data.name" />
|
<Assessments :batch="batch.data.name" />
|
||||||
<StudentHeatmap />
|
<!-- <StudentHeatmap /> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
|
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
|
||||||
import Assessments from '@/components/Assessments.vue'
|
import Assessments from '@/components/Assessments.vue'
|
||||||
import StudentHeatmap from '@/components/StudentHeatmap.vue'
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
batch: {
|
batch: {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
class="flex w-full items-center justify-between focus:outline-none"
|
class="flex w-full items-center justify-between focus:outline-none"
|
||||||
:class="inputClasses"
|
:class="inputClasses"
|
||||||
@click="() => togglePopover()"
|
@click="() => togglePopover()"
|
||||||
|
:disabled="attrs.readonly"
|
||||||
>
|
>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<slot name="prefix" />
|
<slot name="prefix" />
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<div class="text-xs text-ink-gray-5 mb-2">
|
||||||
|
{{ label }}
|
||||||
|
</div>
|
||||||
<div class="overflow-x-auto border rounded-md">
|
<div class="overflow-x-auto border rounded-md">
|
||||||
<div
|
<div
|
||||||
class="grid items-center space-x-4 p-2 border-b"
|
class="grid items-center space-x-4 p-2 border-b"
|
||||||
@@ -14,7 +17,6 @@
|
|||||||
</div>
|
</div>
|
||||||
<div></div>
|
<div></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-for="(row, rowIndex) in rows"
|
v-for="(row, rowIndex) in rows"
|
||||||
:key="rowIndex"
|
:key="rowIndex"
|
||||||
@@ -71,7 +73,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import { Button } from 'frappe-ui'
|
import { Button } from 'frappe-ui'
|
||||||
import { Ellipsis, Plus, Trash2 } from 'lucide-vue-next'
|
import { Ellipsis, Plus, Trash2 } from 'lucide-vue-next'
|
||||||
import { onClickOutside } from '@vueuse/core'
|
import { onClickOutside } from '@vueuse/core'
|
||||||
@@ -93,6 +95,7 @@ const props = withDefaults(
|
|||||||
defineProps<{
|
defineProps<{
|
||||||
modelValue?: Cell[][]
|
modelValue?: Cell[][]
|
||||||
columns?: string[]
|
columns?: string[]
|
||||||
|
label?: string
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
columns: [],
|
columns: [],
|
||||||
@@ -101,6 +104,12 @@ const props = withDefaults(
|
|||||||
|
|
||||||
const columns = ref(props.columns)
|
const columns = ref(props.columns)
|
||||||
|
|
||||||
|
watch(rows, () => {
|
||||||
|
if (rows.value?.length < 1) {
|
||||||
|
addRow()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const addRow = () => {
|
const addRow = () => {
|
||||||
if (!rows.value) {
|
if (!rows.value) {
|
||||||
rows.value = []
|
rows.value = []
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
:variant="attrs.variant"
|
:variant="attrs.variant"
|
||||||
:placeholder="attrs.placeholder"
|
:placeholder="attrs.placeholder"
|
||||||
:filterable="false"
|
:filterable="false"
|
||||||
|
:readonly="attrs.readonly"
|
||||||
>
|
>
|
||||||
<template #target="{ open, togglePopover }">
|
<template #target="{ open, togglePopover }">
|
||||||
<slot name="target" v-bind="{ open, togglePopover }" />
|
<slot name="target" v-bind="{ open, togglePopover }" />
|
||||||
|
|||||||
@@ -138,7 +138,6 @@ watch(userResource, () => {
|
|||||||
) {
|
) {
|
||||||
addQuizzes()
|
addQuizzes()
|
||||||
addAssignments()
|
addAssignments()
|
||||||
addProgrammingExercises()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -158,14 +157,6 @@ const addAssignments = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const addProgrammingExercises = () => {
|
|
||||||
otherLinks.value.push({
|
|
||||||
label: 'Programming Exercises',
|
|
||||||
icon: 'Code',
|
|
||||||
to: 'ProgrammingExercises',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let isActive = (tab) => {
|
let isActive = (tab) => {
|
||||||
return tab.activeFor?.includes(router.currentRoute.value.name)
|
return tab.activeFor?.includes(router.currentRoute.value.name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,6 +255,9 @@ const saveEvaluation = () => {
|
|||||||
}
|
}
|
||||||
toast.success(__('Evaluation saved successfully'))
|
toast.success(__('Evaluation saved successfully'))
|
||||||
},
|
},
|
||||||
|
onError(err) {
|
||||||
|
toast.warning(__(err.messages?.[0] || err))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -277,6 +280,9 @@ const certificateResource = createResource({
|
|||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
certificate.name = data
|
certificate.name = data
|
||||||
},
|
},
|
||||||
|
onError(err) {
|
||||||
|
toast.warning(__(err.messages?.[0] || err))
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const certificateDetails = createResource({
|
const certificateDetails = createResource({
|
||||||
@@ -310,6 +316,9 @@ const saveCertificate = () => {
|
|||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success(__('Certificate saved successfully'))
|
toast.success(__('Certificate saved successfully'))
|
||||||
},
|
},
|
||||||
|
onError(err) {
|
||||||
|
toast.error(__(err.messages?.[0] || err))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
/>
|
/>
|
||||||
<ChildTable
|
<ChildTable
|
||||||
v-model="exercise.test_cases"
|
v-model="exercise.test_cases"
|
||||||
|
:label="__('Test Cases')"
|
||||||
:columns="testCaseColumns"
|
:columns="testCaseColumns"
|
||||||
:required="true"
|
:required="true"
|
||||||
:addable="true"
|
:addable="true"
|
||||||
@@ -52,7 +53,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<template #actions="{ close }">
|
<template #actions="{ close }">
|
||||||
<div class="flex justify-end space-x-2">
|
<div class="flex justify-end space-x-2 group">
|
||||||
|
<Button
|
||||||
|
v-if="exerciseID != 'new'"
|
||||||
|
@click="deleteExercise(close)"
|
||||||
|
variant="outline"
|
||||||
|
theme="red"
|
||||||
|
class="invisible group-hover:visible"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<Trash2 class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Delete') }}
|
||||||
|
</Button>
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'ProgrammingExerciseSubmission',
|
name: 'ProgrammingExerciseSubmission',
|
||||||
@@ -63,6 +76,9 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Button>
|
<Button>
|
||||||
|
<template #prefix>
|
||||||
|
<Play class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
{{ __('Test this Exercise') }}
|
{{ __('Test this Exercise') }}
|
||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -75,6 +91,9 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Button>
|
<Button>
|
||||||
|
<template #prefix>
|
||||||
|
<ClipboardList class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
{{ __('Check Submission') }}
|
{{ __('Check Submission') }}
|
||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -95,22 +114,16 @@ import {
|
|||||||
toast,
|
toast,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { ProgrammingExercise, TestCase } from '@/types/programming-exercise'
|
import {
|
||||||
|
ProgrammingExercise,
|
||||||
|
ProgrammingExercises,
|
||||||
|
TestCase,
|
||||||
|
} from '@/types/programming-exercise'
|
||||||
import ChildTable from '@/components/Controls/ChildTable.vue'
|
import ChildTable from '@/components/Controls/ChildTable.vue'
|
||||||
|
import { ClipboardList, Play, Trash2 } from 'lucide-vue-next'
|
||||||
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
const exercises = defineModel<{
|
const exercises = defineModel<ProgrammingExercises>('exercises')
|
||||||
data: ProgrammingExercise[]
|
|
||||||
reload: () => void
|
|
||||||
hasNextPage: boolean
|
|
||||||
next: () => void
|
|
||||||
setValue: {
|
|
||||||
submit: (
|
|
||||||
data: ProgrammingExercise,
|
|
||||||
options?: { onSuccess?: () => void }
|
|
||||||
) => void
|
|
||||||
}
|
|
||||||
}>('exercises')
|
|
||||||
|
|
||||||
const exercise = ref<ProgrammingExercise>({
|
const exercise = ref<ProgrammingExercise>({
|
||||||
title: '',
|
title: '',
|
||||||
@@ -122,8 +135,6 @@ const exercise = ref<ProgrammingExercise>({
|
|||||||
const languageOptions = [
|
const languageOptions = [
|
||||||
{ label: 'Python', value: 'Python' },
|
{ label: 'Python', value: 'Python' },
|
||||||
{ label: 'JavaScript', value: 'JavaScript' },
|
{ label: 'JavaScript', value: 'JavaScript' },
|
||||||
{ label: 'Java', value: 'Java' },
|
|
||||||
{ label: 'C++', value: 'C++' },
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
@@ -138,19 +149,28 @@ const props = withDefaults(
|
|||||||
watch(
|
watch(
|
||||||
() => props.exerciseID,
|
() => props.exerciseID,
|
||||||
() => {
|
() => {
|
||||||
if (props.exerciseID != 'new') {
|
setExerciseData()
|
||||||
setExerciseData()
|
fetchTestCases()
|
||||||
fetchTestCases()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const setExerciseData = () => {
|
const setExerciseData = () => {
|
||||||
exercises.value?.data.forEach((ex) => {
|
let isNew = true
|
||||||
|
exercises.value?.data.forEach((ex: ProgrammingExercise) => {
|
||||||
if (ex.name === props.exerciseID) {
|
if (ex.name === props.exerciseID) {
|
||||||
|
isNew = false
|
||||||
exercise.value = { ...ex }
|
exercise.value = { ...ex }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
exercise.value = {
|
||||||
|
title: '',
|
||||||
|
language: 'Python',
|
||||||
|
problem_statement: '',
|
||||||
|
test_cases: [],
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const testCases = createListResource({
|
const testCases = createListResource({
|
||||||
@@ -180,22 +200,25 @@ const saveExercise = (close: () => void) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewExercise = (close: () => void) => {
|
const createNewExercise = (close: () => void) => {
|
||||||
exercises.value.insert.submit(
|
exercises.value?.insert.submit(
|
||||||
{
|
{
|
||||||
...exercise.value,
|
...exercise.value,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
close()
|
close()
|
||||||
exercises.value.reload()
|
exercises.value?.reload()
|
||||||
toast.success(__('Programming Exercise created successfully'))
|
toast.success(__('Programming Exercise created successfully'))
|
||||||
},
|
},
|
||||||
|
onError(err: any) {
|
||||||
|
toast.warning(__(err.messages?.[0] || err))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateExercise = (close: () => void) => {
|
const updateExercise = (close: () => void) => {
|
||||||
exercises.value.setValue.submit(
|
exercises.value?.setValue.submit(
|
||||||
{
|
{
|
||||||
name: props.exerciseID,
|
name: props.exerciseID,
|
||||||
...exercise.value,
|
...exercise.value,
|
||||||
@@ -203,9 +226,12 @@ const updateExercise = (close: () => void) => {
|
|||||||
{
|
{
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
close()
|
close()
|
||||||
exercises.value.reload()
|
exercises.value?.reload()
|
||||||
toast.success(__('Programming Exercise updated successfully'))
|
toast.success(__('Programming Exercise updated successfully'))
|
||||||
},
|
},
|
||||||
|
onError(err: any) {
|
||||||
|
toast.warning(__(err.messages?.[0] || err))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -213,4 +239,17 @@ const updateExercise = (close: () => void) => {
|
|||||||
const testCaseColumns = computed(() => {
|
const testCaseColumns = computed(() => {
|
||||||
return ['Input', 'Expected Output']
|
return ['Input', 'Expected Output']
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const deleteExercise = (close: () => void) => {
|
||||||
|
if (props.exerciseID == 'new') return
|
||||||
|
exercises.value?.delete.submit(props.exerciseID, {
|
||||||
|
onSuccess() {
|
||||||
|
toast.success(__('Programming Exercise deleted successfully'))
|
||||||
|
close()
|
||||||
|
},
|
||||||
|
onError(err: any) {
|
||||||
|
toast.warning(__(err.messages?.[0] || err))
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -6,10 +6,15 @@
|
|||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</header>
|
</header>
|
||||||
<div class="grid grid-cols-2 h-[calc(100vh_-_3rem)]">
|
<div class="grid grid-cols-2 h-[calc(100vh_-_3rem)]">
|
||||||
<div
|
<div class="border-r py-5 px-8 h-full">
|
||||||
v-html="exercise.doc?.problem_statement"
|
<div class="font-semibold mb-2">
|
||||||
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"
|
{{ __('Problem Statement') }}
|
||||||
></div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-html="exercise.doc?.problem_statement"
|
||||||
|
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"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div class="flex items-center justify-between p-2 bg-surface-gray-2">
|
<div class="flex items-center justify-between p-2 bg-surface-gray-2">
|
||||||
<div class="font-semibold">
|
<div class="font-semibold">
|
||||||
@@ -22,7 +27,13 @@
|
|||||||
>
|
>
|
||||||
{{ submission.doc.status }}
|
{{ submission.doc.status }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button variant="solid" @click="submitCode">
|
<Button
|
||||||
|
v-if="
|
||||||
|
submissionID == 'new' || user.data?.name == submission.doc?.owner
|
||||||
|
"
|
||||||
|
variant="solid"
|
||||||
|
@click="submitCode"
|
||||||
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<Play class="size-3" />
|
<Play class="size-3" />
|
||||||
</template>
|
</template>
|
||||||
@@ -33,7 +44,7 @@
|
|||||||
<div class="flex flex-col space-y-4 py-5 border-b">
|
<div class="flex flex-col space-y-4 py-5 border-b">
|
||||||
<Code
|
<Code
|
||||||
v-model="code"
|
v-model="code"
|
||||||
language="python"
|
:language="exercise.doc?.language.toLowerCase()"
|
||||||
height="400px"
|
height="400px"
|
||||||
maxHeight="1000px"
|
maxHeight="1000px"
|
||||||
/>
|
/>
|
||||||
@@ -43,23 +54,23 @@
|
|||||||
<textarea
|
<textarea
|
||||||
v-if="error"
|
v-if="error"
|
||||||
v-model="errorMessage"
|
v-model="errorMessage"
|
||||||
class="bg-surface-gray-1 border-none text-sm h-28 leading-6"
|
class="bg-surface-gray-1 border-none text-sm h-32 leading-6"
|
||||||
readonly
|
readonly
|
||||||
/>
|
/>
|
||||||
<!-- <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" class="p-5">
|
||||||
<span class="text-lg font-semibold text-ink-gray-9">
|
<span class="text-lg font-semibold text-ink-gray-9">
|
||||||
{{ __('Test Cases') }}
|
{{ __('Test Cases') }}
|
||||||
</span>
|
</span>
|
||||||
<div class="divide-y mt-5">
|
<div v-if="testCases.length" class="divide-y mt-5">
|
||||||
<div
|
<div
|
||||||
v-for="(testCase, index) in testCases"
|
v-for="(testCase, index) in testCases"
|
||||||
:key="testCase.input"
|
:key="testCase.input"
|
||||||
class="py-3"
|
class="py-3"
|
||||||
>
|
>
|
||||||
<div class="flex items-center mb-5">
|
<div class="flex items-center mb-3">
|
||||||
<span class=""> {{ __('Test {0}').format(index + 1) }} - </span>
|
<span class=""> {{ __('Test {0}').format(index + 1) }} - </span>
|
||||||
<span
|
<span
|
||||||
class="font-semibold ml-2 mr-1"
|
class="font-semibold ml-2 mr-1"
|
||||||
@@ -71,16 +82,18 @@
|
|||||||
>
|
>
|
||||||
{{ testCase.status }}
|
{{ testCase.status }}
|
||||||
</span>
|
</span>
|
||||||
<span v-if="testCase.status === 'Passed'">
|
<!-- <span v-if="testCase.status === 'Passed'">
|
||||||
<Check class="size-4 text-ink-green-3" />
|
<Check class="size-4 text-ink-green-3" />
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
<X class="size-4 text-ink-red-3" />
|
<X class="size-4 text-ink-red-3" />
|
||||||
</span>
|
</span> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between w-[60%]">
|
<div class="flex items-center justify-between w-[60%]">
|
||||||
<div v-if="testCase.input" class="space-y-2">
|
<div v-if="testCase.input" class="space-y-2">
|
||||||
<div class="text-xs text-ink-gray-7">{{ __('Input') }}:</div>
|
<div class="text-xs text-ink-gray-7">
|
||||||
|
{{ __('Input') }}
|
||||||
|
</div>
|
||||||
<div>{{ testCase.input }}</div>
|
<div>{{ testCase.input }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
@@ -100,6 +113,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else class="text-sm text-ink-gray-6 mt-4">
|
||||||
|
{{ __('Please run the code to execute the test cases.') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,11 +130,12 @@ import {
|
|||||||
toast,
|
toast,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||||
import { Play, X, Check } from 'lucide-vue-next'
|
import { Play, X, Check } from 'lucide-vue-next'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const user = inject<any>('$user')
|
||||||
const code = ref<string | null>('')
|
const code = ref<string | null>('')
|
||||||
const output = ref<string | null>(null)
|
const output = ref<string | null>(null)
|
||||||
const error = ref<boolean | null>(null)
|
const error = ref<boolean | null>(null)
|
||||||
@@ -132,12 +149,11 @@ const testCases = ref<
|
|||||||
status: string
|
status: string
|
||||||
}>
|
}>
|
||||||
>([])
|
>([])
|
||||||
const boilerplate = ref<string>(
|
const boilerplate = ref<string>('')
|
||||||
`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()
|
||||||
const fromLesson = ref(false)
|
const fromLesson = ref(false)
|
||||||
|
const falconURL = 'https://falcon.frappe.io/'
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -151,16 +167,25 @@ const props = withDefaults(
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadFalcon()
|
loadFalcon()
|
||||||
if (props.submissionID != 'new') {
|
checkIfUserIsPermitted()
|
||||||
submission.reload()
|
checkIfInLesson()
|
||||||
}
|
fetchSubmission()
|
||||||
if (!code.value) {
|
})
|
||||||
code.value = boilerplate.value
|
|
||||||
}
|
const checkIfInLesson = () => {
|
||||||
if (new URLSearchParams(window.location.search).get('fromLesson')) {
|
if (new URLSearchParams(window.location.search).get('fromLesson')) {
|
||||||
fromLesson.value = true
|
fromLesson.value = true
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const fetchSubmission = (name: string = '') => {
|
||||||
|
if (name) {
|
||||||
|
submission.name = name
|
||||||
|
submission.reload()
|
||||||
|
} else if (props.submissionID != 'new') {
|
||||||
|
submission.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const exercise = createDocumentResource({
|
const exercise = createDocumentResource({
|
||||||
doctype: 'LMS Programming Exercise',
|
doctype: 'LMS Programming Exercise',
|
||||||
@@ -172,17 +197,76 @@ const exercise = createDocumentResource({
|
|||||||
const submission = createDocumentResource({
|
const submission = createDocumentResource({
|
||||||
doctype: 'LMS Programming Exercise Submission',
|
doctype: 'LMS Programming Exercise Submission',
|
||||||
name: props.submissionID,
|
name: props.submissionID,
|
||||||
cache: ['programmingExerciseSubmission', props.submissionID],
|
auto: false,
|
||||||
|
onError(error: any) {
|
||||||
|
if (error.messages?.[0].includes('not found')) {
|
||||||
|
router.push({
|
||||||
|
name: 'ProgrammingExerciseSubmission',
|
||||||
|
params: { exerciseID: props.exerciseID, submissionID: 'new' },
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
toast.error(__(error.messages?.[0] || error))
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(exercise, () => {
|
||||||
|
updateCode()
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateCode = (submissionCode = '') => {
|
||||||
|
updateBoilerPlate()
|
||||||
|
if (!code.value?.includes(boilerplate.value)) {
|
||||||
|
code.value = `${boilerplate.value}${code.value}`
|
||||||
|
}
|
||||||
|
if (submissionCode && !code.value?.includes(submissionCode)) {
|
||||||
|
code.value = `${code.value}${submissionCode}`
|
||||||
|
} else if (!submissionCode && !code.value) {
|
||||||
|
code.value = boilerplate.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateBoilerPlate = () => {
|
||||||
|
if (exercise.doc?.language == 'Python') {
|
||||||
|
boilerplate.value = `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`
|
||||||
|
} else if (exercise.doc?.language == 'JavaScript') {
|
||||||
|
boilerplate.value = `const fs = require('fs');\n\nlet input = fs.readFileSync('/app/stdin', 'utf8').trim();\nconst inputs = input.split("\\n");\n// inputs is an array of strings\n// write your code below\n`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkIfUserIsPermitted = (doc: any = null) => {
|
||||||
|
if (!user.data) {
|
||||||
|
window.location.href = `/login?redirect-to=/lms/programming-exercises/${props.exerciseID}/submission/${props.submissionID}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doc) return
|
||||||
|
if (
|
||||||
|
doc.owner != user.data?.name &&
|
||||||
|
!user.data?.is_instructor &&
|
||||||
|
!user.data?.is_moderator &&
|
||||||
|
!user.data.is_evaluator
|
||||||
|
) {
|
||||||
|
router.push({
|
||||||
|
name: 'ProgrammingExerciseSubmission',
|
||||||
|
params: { exerciseID: props.exerciseID, submissionID: 'new' },
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTestCases = (doc: any) => {
|
||||||
|
if (testCases.value.length === 0) {
|
||||||
|
testCases.value = doc.test_cases || []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => submission.doc,
|
() => submission.doc,
|
||||||
(doc) => {
|
(doc) => {
|
||||||
if (doc) {
|
if (doc) {
|
||||||
code.value = `${boilerplate.value}${doc.code || ''}\n`
|
checkIfUserIsPermitted(doc)
|
||||||
if (testCases.value.length === 0) {
|
updateTestCases(doc)
|
||||||
testCases.value = doc.test_cases || []
|
updateCode(doc.code)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
@@ -191,7 +275,7 @@ watch(
|
|||||||
const loadFalcon = () => {
|
const loadFalcon = () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const script = document.createElement('script')
|
const script = document.createElement('script')
|
||||||
script.src = 'https://falcon.frappe.io/static/livecode.js'
|
script.src = `${falconURL}static/livecode.js`
|
||||||
script.onload = resolve
|
script.onload = resolve
|
||||||
script.onerror = reject
|
script.onerror = reject
|
||||||
document.head.appendChild(script)
|
document.head.appendChild(script)
|
||||||
@@ -246,10 +330,11 @@ const createSubmission = () => {
|
|||||||
name: 'ProgrammingExerciseSubmission',
|
name: 'ProgrammingExerciseSubmission',
|
||||||
params: { exerciseID: props.exerciseID, submissionID: data },
|
params: { exerciseID: props.exerciseID, submissionID: data },
|
||||||
})
|
})
|
||||||
|
fetchSubmission(data)
|
||||||
} else {
|
} else {
|
||||||
submission.reload()
|
fetchSubmission(props.submissionID)
|
||||||
}
|
}
|
||||||
toast.success(__('Submitted successfully!'))
|
toast.success(__('Submission saved!'))
|
||||||
})
|
})
|
||||||
.catch((error: any) => {
|
.catch((error: any) => {
|
||||||
console.error('Error creating submission:', error)
|
console.error('Error creating submission:', error)
|
||||||
@@ -267,7 +352,7 @@ const execute = (stdin = ''): Promise<string> => {
|
|||||||
|
|
||||||
let session = new LiveCodeSession({
|
let session = new LiveCodeSession({
|
||||||
base_url: 'https://falcon.frappe.io',
|
base_url: 'https://falcon.frappe.io',
|
||||||
runtime: 'python',
|
runtime: exercise.doc?.language.toLowerCase() || 'python',
|
||||||
code: code.value,
|
code: code.value,
|
||||||
files: [{ filename: 'stdin', contents: stdin }],
|
files: [{ filename: 'stdin', contents: stdin }],
|
||||||
onMessage: (msg: any) => {
|
onMessage: (msg: any) => {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
doctype="User"
|
doctype="User"
|
||||||
v-model="filters.member"
|
v-model="filters.member"
|
||||||
:placeholder="__('Filter by Member')"
|
:placeholder="__('Filter by Member')"
|
||||||
|
:readonly="isStudent"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="filters.status"
|
v-model="filters.status"
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
:rows="submissions.data"
|
:rows="submissions.data"
|
||||||
rowKey="name"
|
rowKey="name"
|
||||||
:options="{
|
:options="{
|
||||||
selectable: false,
|
selectable: true,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<ListHeader
|
<ListHeader
|
||||||
@@ -100,6 +101,18 @@
|
|||||||
</ListRow>
|
</ListRow>
|
||||||
</router-link>
|
</router-link>
|
||||||
</ListRows>
|
</ListRows>
|
||||||
|
<ListSelectBanner>
|
||||||
|
<template #actions="{ unselectAll, selections }">
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
@click="deleteExercises(selections, unselectAll)"
|
||||||
|
>
|
||||||
|
<Trash2 class="h-4 w-4 stroke-1.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</ListSelectBanner>
|
||||||
</ListView>
|
</ListView>
|
||||||
<EmptyState v-else type="Programming Exercise Submissions" />
|
<EmptyState v-else type="Programming Exercise Submissions" />
|
||||||
<div
|
<div
|
||||||
@@ -127,7 +140,9 @@ import {
|
|||||||
ListRows,
|
ListRows,
|
||||||
ListRow,
|
ListRow,
|
||||||
ListRowItem,
|
ListRowItem,
|
||||||
|
ListSelectBanner,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
|
toast,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import type {
|
import type {
|
||||||
ProgrammingExerciseSubmission,
|
ProgrammingExerciseSubmission,
|
||||||
@@ -136,6 +151,7 @@ import type {
|
|||||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { Trash2 } from 'lucide-vue-next'
|
||||||
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'
|
||||||
|
|
||||||
@@ -151,9 +167,11 @@ const filters = ref<Filters>({
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!user.data?.is_instructor && !user.data?.is_moderator) {
|
setFiltersFromRoute()
|
||||||
router.push({ name: 'Courses' })
|
fetchBasedOnRole()
|
||||||
}
|
})
|
||||||
|
|
||||||
|
const setFiltersFromRoute = () => {
|
||||||
filterFields.forEach((field) => {
|
filterFields.forEach((field) => {
|
||||||
if (router.currentRoute.value.query[field]) {
|
if (router.currentRoute.value.query[field]) {
|
||||||
filters.value[field as keyof Filters] = router.currentRoute.value.query[
|
filters.value[field as keyof Filters] = router.currentRoute.value.query[
|
||||||
@@ -161,7 +179,15 @@ onMounted(() => {
|
|||||||
] as string
|
] as string
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
})
|
}
|
||||||
|
|
||||||
|
const fetchBasedOnRole = () => {
|
||||||
|
if (isStudent.value) {
|
||||||
|
filters.value['member'] = user.data?.name
|
||||||
|
} else {
|
||||||
|
submissions.reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const submissions = createListResource({
|
const submissions = createListResource({
|
||||||
doctype: 'LMS Programming Exercise Submission',
|
doctype: 'LMS Programming Exercise Submission',
|
||||||
@@ -174,7 +200,7 @@ const submissions = createListResource({
|
|||||||
'status',
|
'status',
|
||||||
'modified',
|
'modified',
|
||||||
],
|
],
|
||||||
auto: true,
|
orderBy: 'modified desc',
|
||||||
transform(data: ProgrammingExercise[]) {
|
transform(data: ProgrammingExercise[]) {
|
||||||
return data.map((submission: ProgrammingExerciseSubmission) => {
|
return data.map((submission: ProgrammingExerciseSubmission) => {
|
||||||
return {
|
return {
|
||||||
@@ -214,6 +240,22 @@ watch(filters.value, () => {
|
|||||||
submissions.reload()
|
submissions.reload()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const deleteExercises = (selections: Set<string>, unselectAll: () => void) => {
|
||||||
|
Array.from(selections).forEach(async (submission: string) => {
|
||||||
|
await submissions.delete.submit(submission)
|
||||||
|
})
|
||||||
|
unselectAll()
|
||||||
|
toast.success(__('Submissions deleted successfully'))
|
||||||
|
}
|
||||||
|
|
||||||
|
const isStudent = computed(() => {
|
||||||
|
return (
|
||||||
|
!user.data?.is_instructor &&
|
||||||
|
!user.data?.is_moderator &&
|
||||||
|
!user.data?.is_evaluator
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const submissionColumns = computed(() => {
|
const submissionColumns = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -81,7 +81,7 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
import {
|
import {
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
Button,
|
Button,
|
||||||
@@ -91,6 +91,7 @@ import {
|
|||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { Plus } from 'lucide-vue-next'
|
import { Plus } from 'lucide-vue-next'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
import ProgrammingExerciseForm from '@/pages/ProgrammingExercises/ProgrammingExerciseForm.vue'
|
import ProgrammingExerciseForm from '@/pages/ProgrammingExercises/ProgrammingExerciseForm.vue'
|
||||||
|
|
||||||
const exerciseCount = ref<number>(0)
|
const exerciseCount = ref<number>(0)
|
||||||
@@ -98,11 +99,26 @@ const readOnlyMode = window.read_only_mode
|
|||||||
const { brand } = sessionStore()
|
const { brand } = sessionStore()
|
||||||
const showForm = ref<boolean>(false)
|
const showForm = ref<boolean>(false)
|
||||||
const exerciseID = ref<string | null>('new')
|
const exerciseID = ref<string | null>('new')
|
||||||
|
const user = inject<any>('$user')
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
validatePermissions()
|
||||||
getExerciseCount()
|
getExerciseCount()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const validatePermissions = () => {
|
||||||
|
if (
|
||||||
|
!user.data?.is_instructor &&
|
||||||
|
!user.data?.is_moderator &&
|
||||||
|
!user.data?.is_evaluator
|
||||||
|
) {
|
||||||
|
router.push({
|
||||||
|
name: 'ProgrammingExerciseSubmissions',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const getExerciseCount = () => {
|
const getExerciseCount = () => {
|
||||||
call('frappe.client.get_count', {
|
call('frappe.client.get_count', {
|
||||||
doctype: 'LMS Programming Exercise',
|
doctype: 'LMS Programming Exercise',
|
||||||
|
|||||||
@@ -19,4 +19,29 @@ type Filters = {
|
|||||||
exercise?: string,
|
exercise?: string,
|
||||||
member?: string,
|
member?: string,
|
||||||
status?: string
|
status?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProgrammingExercises = {
|
||||||
|
data: ProgrammingExercise[]
|
||||||
|
reload: () => void
|
||||||
|
hasNextPage: boolean
|
||||||
|
next: () => void
|
||||||
|
setValue: {
|
||||||
|
submit: (
|
||||||
|
data: ProgrammingExercise,
|
||||||
|
options?: { onSuccess?: () => void }
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
insert: {
|
||||||
|
submit: (
|
||||||
|
data: ProgrammingExercise,
|
||||||
|
options?: { onSuccess?: () => void }
|
||||||
|
) => void
|
||||||
|
}
|
||||||
|
delete: {
|
||||||
|
submit: (
|
||||||
|
name: string,
|
||||||
|
options?: { onSuccess?: () => void }
|
||||||
|
) => void
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -439,6 +439,17 @@ export function getSidebarLinks() {
|
|||||||
to: 'Batches',
|
to: 'Batches',
|
||||||
activeFor: ['Batches', 'BatchDetail', 'Batch', 'BatchForm'],
|
activeFor: ['Batches', 'BatchDetail', 'Batch', 'BatchForm'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Programming Exercises',
|
||||||
|
icon: 'Code',
|
||||||
|
to: 'ProgrammingExercises',
|
||||||
|
activeFor: [
|
||||||
|
'ProgrammingExercises',
|
||||||
|
'ProgrammingExerciseForm',
|
||||||
|
'ProgrammingExerciseSubmissions',
|
||||||
|
'ProgrammingExerciseSubmission',
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Certified Members',
|
label: 'Certified Members',
|
||||||
icon: 'GraduationCap',
|
icon: 'GraduationCap',
|
||||||
|
|||||||
@@ -63,7 +63,7 @@
|
|||||||
"link_fieldname": "exercise"
|
"link_fieldname": "exercise"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-06-20 12:53:59.295679",
|
"modified": "2025-06-24 14:42:27.463492",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Programming Exercise",
|
"name": "LMS Programming Exercise",
|
||||||
@@ -104,6 +104,27 @@
|
|||||||
"role": "Course Creator",
|
"role": "Course Creator",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Batch Evaluator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "LMS Student",
|
||||||
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"row_format": "Dynamic",
|
"row_format": "Dynamic",
|
||||||
|
|||||||
@@ -1,9 +1,15 @@
|
|||||||
# Copyright (c) 2025, Frappe and contributors
|
# Copyright (c) 2025, Frappe and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
# import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
class LMSProgrammingExercise(Document):
|
class LMSProgrammingExercise(Document):
|
||||||
pass
|
def validate(self):
|
||||||
|
self.validate_test_cases()
|
||||||
|
|
||||||
|
def validate_test_cases(self):
|
||||||
|
if not self.test_cases:
|
||||||
|
frappe.throw(_("At least one test case is required for the programming exercise."))
|
||||||
|
|||||||
@@ -88,7 +88,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-06-20 19:17:23.940979",
|
"modified": "2025-06-24 14:42:08.288983",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Programming Exercise Submission",
|
"name": "LMS Programming Exercise Submission",
|
||||||
@@ -105,6 +105,53 @@
|
|||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Moderator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Course Creator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"delete": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Batch Evaluator",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "LMS Student",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"row_format": "Dynamic",
|
"row_format": "Dynamic",
|
||||||
|
|||||||
Reference in New Issue
Block a user