feat: javascript exercises
This commit is contained in:
@@ -26,6 +26,7 @@
|
||||
/>
|
||||
<ChildTable
|
||||
v-model="exercise.test_cases"
|
||||
:label="__('Test Cases')"
|
||||
:columns="testCaseColumns"
|
||||
:required="true"
|
||||
:addable="true"
|
||||
@@ -52,7 +53,19 @@
|
||||
</div>
|
||||
</template>
|
||||
<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
|
||||
:to="{
|
||||
name: 'ProgrammingExerciseSubmission',
|
||||
@@ -63,6 +76,9 @@
|
||||
}"
|
||||
>
|
||||
<Button>
|
||||
<template #prefix>
|
||||
<Play class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Test this Exercise') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
@@ -75,6 +91,9 @@
|
||||
}"
|
||||
>
|
||||
<Button>
|
||||
<template #prefix>
|
||||
<ClipboardList class="size-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Check Submission') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
@@ -95,22 +114,16 @@ import {
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
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 { ClipboardList, Play, Trash2 } from 'lucide-vue-next'
|
||||
|
||||
const show = defineModel()
|
||||
const exercises = defineModel<{
|
||||
data: ProgrammingExercise[]
|
||||
reload: () => void
|
||||
hasNextPage: boolean
|
||||
next: () => void
|
||||
setValue: {
|
||||
submit: (
|
||||
data: ProgrammingExercise,
|
||||
options?: { onSuccess?: () => void }
|
||||
) => void
|
||||
}
|
||||
}>('exercises')
|
||||
const exercises = defineModel<ProgrammingExercises>('exercises')
|
||||
|
||||
const exercise = ref<ProgrammingExercise>({
|
||||
title: '',
|
||||
@@ -122,8 +135,6 @@ const exercise = ref<ProgrammingExercise>({
|
||||
const languageOptions = [
|
||||
{ label: 'Python', value: 'Python' },
|
||||
{ label: 'JavaScript', value: 'JavaScript' },
|
||||
{ label: 'Java', value: 'Java' },
|
||||
{ label: 'C++', value: 'C++' },
|
||||
]
|
||||
|
||||
const props = withDefaults(
|
||||
@@ -138,19 +149,28 @@ const props = withDefaults(
|
||||
watch(
|
||||
() => props.exerciseID,
|
||||
() => {
|
||||
if (props.exerciseID != 'new') {
|
||||
setExerciseData()
|
||||
fetchTestCases()
|
||||
}
|
||||
setExerciseData()
|
||||
fetchTestCases()
|
||||
}
|
||||
)
|
||||
|
||||
const setExerciseData = () => {
|
||||
exercises.value?.data.forEach((ex) => {
|
||||
let isNew = true
|
||||
exercises.value?.data.forEach((ex: ProgrammingExercise) => {
|
||||
if (ex.name === props.exerciseID) {
|
||||
isNew = false
|
||||
exercise.value = { ...ex }
|
||||
}
|
||||
})
|
||||
|
||||
if (isNew) {
|
||||
exercise.value = {
|
||||
title: '',
|
||||
language: 'Python',
|
||||
problem_statement: '',
|
||||
test_cases: [],
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const testCases = createListResource({
|
||||
@@ -180,22 +200,25 @@ const saveExercise = (close: () => void) => {
|
||||
}
|
||||
|
||||
const createNewExercise = (close: () => void) => {
|
||||
exercises.value.insert.submit(
|
||||
exercises.value?.insert.submit(
|
||||
{
|
||||
...exercise.value,
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
close()
|
||||
exercises.value.reload()
|
||||
exercises.value?.reload()
|
||||
toast.success(__('Programming Exercise created successfully'))
|
||||
},
|
||||
onError(err: any) {
|
||||
toast.warning(__(err.messages?.[0] || err))
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const updateExercise = (close: () => void) => {
|
||||
exercises.value.setValue.submit(
|
||||
exercises.value?.setValue.submit(
|
||||
{
|
||||
name: props.exerciseID,
|
||||
...exercise.value,
|
||||
@@ -203,9 +226,12 @@ const updateExercise = (close: () => void) => {
|
||||
{
|
||||
onSuccess() {
|
||||
close()
|
||||
exercises.value.reload()
|
||||
exercises.value?.reload()
|
||||
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(() => {
|
||||
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>
|
||||
|
||||
@@ -6,10 +6,15 @@
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
</header>
|
||||
<div class="grid grid-cols-2 h-[calc(100vh_-_3rem)]">
|
||||
<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 border-r px-5 py-2 h-full"
|
||||
></div>
|
||||
<div class="border-r py-5 px-8 h-full">
|
||||
<div class="font-semibold mb-2">
|
||||
{{ __('Problem Statement') }}
|
||||
</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 class="flex items-center justify-between p-2 bg-surface-gray-2">
|
||||
<div class="font-semibold">
|
||||
@@ -22,7 +27,13 @@
|
||||
>
|
||||
{{ submission.doc.status }}
|
||||
</Badge>
|
||||
<Button variant="solid" @click="submitCode">
|
||||
<Button
|
||||
v-if="
|
||||
submissionID == 'new' || user.data?.name == submission.doc?.owner
|
||||
"
|
||||
variant="solid"
|
||||
@click="submitCode"
|
||||
>
|
||||
<template #prefix>
|
||||
<Play class="size-3" />
|
||||
</template>
|
||||
@@ -33,7 +44,7 @@
|
||||
<div class="flex flex-col space-y-4 py-5 border-b">
|
||||
<Code
|
||||
v-model="code"
|
||||
language="python"
|
||||
:language="exercise.doc?.language.toLowerCase()"
|
||||
height="400px"
|
||||
maxHeight="1000px"
|
||||
/>
|
||||
@@ -43,23 +54,23 @@
|
||||
<textarea
|
||||
v-if="error"
|
||||
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
|
||||
/>
|
||||
<!-- <textarea v-else v-model="output" class="bg-surface-gray-1 border-none text-sm h-28 leading-6" readonly /> -->
|
||||
</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">
|
||||
{{ __('Test Cases') }}
|
||||
</span>
|
||||
<div class="divide-y mt-5">
|
||||
<div v-if="testCases.length" class="divide-y mt-5">
|
||||
<div
|
||||
v-for="(testCase, index) in testCases"
|
||||
:key="testCase.input"
|
||||
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="font-semibold ml-2 mr-1"
|
||||
@@ -71,16 +82,18 @@
|
||||
>
|
||||
{{ testCase.status }}
|
||||
</span>
|
||||
<span v-if="testCase.status === 'Passed'">
|
||||
<!-- <span v-if="testCase.status === 'Passed'">
|
||||
<Check class="size-4 text-ink-green-3" />
|
||||
</span>
|
||||
<span v-else>
|
||||
<X class="size-4 text-ink-red-3" />
|
||||
</span>
|
||||
</span> -->
|
||||
</div>
|
||||
<div class="flex items-center justify-between w-[60%]">
|
||||
<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>
|
||||
<div class="space-y-2">
|
||||
@@ -100,6 +113,9 @@
|
||||
</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>
|
||||
@@ -114,11 +130,12 @@ import {
|
||||
toast,
|
||||
usePageMeta,
|
||||
} 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 { sessionStore } from '@/stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const user = inject<any>('$user')
|
||||
const code = ref<string | null>('')
|
||||
const output = ref<string | null>(null)
|
||||
const error = ref<boolean | null>(null)
|
||||
@@ -132,12 +149,11 @@ const testCases = ref<
|
||||
status: 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 boilerplate = ref<string>('')
|
||||
const { brand } = sessionStore()
|
||||
const router = useRouter()
|
||||
const fromLesson = ref(false)
|
||||
const falconURL = 'https://falcon.frappe.io/'
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -151,16 +167,25 @@ const props = withDefaults(
|
||||
|
||||
onMounted(() => {
|
||||
loadFalcon()
|
||||
if (props.submissionID != 'new') {
|
||||
submission.reload()
|
||||
}
|
||||
if (!code.value) {
|
||||
code.value = boilerplate.value
|
||||
}
|
||||
checkIfUserIsPermitted()
|
||||
checkIfInLesson()
|
||||
fetchSubmission()
|
||||
})
|
||||
|
||||
const checkIfInLesson = () => {
|
||||
if (new URLSearchParams(window.location.search).get('fromLesson')) {
|
||||
fromLesson.value = true
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const fetchSubmission = (name: string = '') => {
|
||||
if (name) {
|
||||
submission.name = name
|
||||
submission.reload()
|
||||
} else if (props.submissionID != 'new') {
|
||||
submission.reload()
|
||||
}
|
||||
}
|
||||
|
||||
const exercise = createDocumentResource({
|
||||
doctype: 'LMS Programming Exercise',
|
||||
@@ -172,17 +197,76 @@ const exercise = createDocumentResource({
|
||||
const submission = createDocumentResource({
|
||||
doctype: 'LMS Programming Exercise Submission',
|
||||
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(
|
||||
() => submission.doc,
|
||||
(doc) => {
|
||||
if (doc) {
|
||||
code.value = `${boilerplate.value}${doc.code || ''}\n`
|
||||
if (testCases.value.length === 0) {
|
||||
testCases.value = doc.test_cases || []
|
||||
}
|
||||
checkIfUserIsPermitted(doc)
|
||||
updateTestCases(doc)
|
||||
updateCode(doc.code)
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
@@ -191,7 +275,7 @@ watch(
|
||||
const loadFalcon = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const script = document.createElement('script')
|
||||
script.src = 'https://falcon.frappe.io/static/livecode.js'
|
||||
script.src = `${falconURL}static/livecode.js`
|
||||
script.onload = resolve
|
||||
script.onerror = reject
|
||||
document.head.appendChild(script)
|
||||
@@ -246,10 +330,11 @@ const createSubmission = () => {
|
||||
name: 'ProgrammingExerciseSubmission',
|
||||
params: { exerciseID: props.exerciseID, submissionID: data },
|
||||
})
|
||||
fetchSubmission(data)
|
||||
} else {
|
||||
submission.reload()
|
||||
fetchSubmission(props.submissionID)
|
||||
}
|
||||
toast.success(__('Submitted successfully!'))
|
||||
toast.success(__('Submission saved!'))
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Error creating submission:', error)
|
||||
@@ -267,7 +352,7 @@ const execute = (stdin = ''): Promise<string> => {
|
||||
|
||||
let session = new LiveCodeSession({
|
||||
base_url: 'https://falcon.frappe.io',
|
||||
runtime: 'python',
|
||||
runtime: exercise.doc?.language.toLowerCase() || 'python',
|
||||
code: code.value,
|
||||
files: [{ filename: 'stdin', contents: stdin }],
|
||||
onMessage: (msg: any) => {
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
doctype="User"
|
||||
v-model="filters.member"
|
||||
:placeholder="__('Filter by Member')"
|
||||
:readonly="isStudent"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="filters.status"
|
||||
@@ -45,7 +46,7 @@
|
||||
:rows="submissions.data"
|
||||
rowKey="name"
|
||||
:options="{
|
||||
selectable: false,
|
||||
selectable: true,
|
||||
}"
|
||||
>
|
||||
<ListHeader
|
||||
@@ -100,6 +101,18 @@
|
||||
</ListRow>
|
||||
</router-link>
|
||||
</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>
|
||||
<EmptyState v-else type="Programming Exercise Submissions" />
|
||||
<div
|
||||
@@ -127,7 +140,9 @@ import {
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListRowItem,
|
||||
ListSelectBanner,
|
||||
usePageMeta,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import type {
|
||||
ProgrammingExerciseSubmission,
|
||||
@@ -136,6 +151,7 @@ import type {
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Trash2 } from 'lucide-vue-next'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
|
||||
@@ -151,9 +167,11 @@ const filters = ref<Filters>({
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
if (!user.data?.is_instructor && !user.data?.is_moderator) {
|
||||
router.push({ name: 'Courses' })
|
||||
}
|
||||
setFiltersFromRoute()
|
||||
fetchBasedOnRole()
|
||||
})
|
||||
|
||||
const setFiltersFromRoute = () => {
|
||||
filterFields.forEach((field) => {
|
||||
if (router.currentRoute.value.query[field]) {
|
||||
filters.value[field as keyof Filters] = router.currentRoute.value.query[
|
||||
@@ -161,7 +179,15 @@ onMounted(() => {
|
||||
] as string
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const fetchBasedOnRole = () => {
|
||||
if (isStudent.value) {
|
||||
filters.value['member'] = user.data?.name
|
||||
} else {
|
||||
submissions.reload()
|
||||
}
|
||||
}
|
||||
|
||||
const submissions = createListResource({
|
||||
doctype: 'LMS Programming Exercise Submission',
|
||||
@@ -174,7 +200,7 @@ const submissions = createListResource({
|
||||
'status',
|
||||
'modified',
|
||||
],
|
||||
auto: true,
|
||||
orderBy: 'modified desc',
|
||||
transform(data: ProgrammingExercise[]) {
|
||||
return data.map((submission: ProgrammingExerciseSubmission) => {
|
||||
return {
|
||||
@@ -214,6 +240,22 @@ watch(filters.value, () => {
|
||||
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(() => {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
@@ -91,6 +91,7 @@ import {
|
||||
} from 'frappe-ui'
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ProgrammingExerciseForm from '@/pages/ProgrammingExercises/ProgrammingExerciseForm.vue'
|
||||
|
||||
const exerciseCount = ref<number>(0)
|
||||
@@ -98,11 +99,26 @@ const readOnlyMode = window.read_only_mode
|
||||
const { brand } = sessionStore()
|
||||
const showForm = ref<boolean>(false)
|
||||
const exerciseID = ref<string | null>('new')
|
||||
const user = inject<any>('$user')
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
validatePermissions()
|
||||
getExerciseCount()
|
||||
})
|
||||
|
||||
const validatePermissions = () => {
|
||||
if (
|
||||
!user.data?.is_instructor &&
|
||||
!user.data?.is_moderator &&
|
||||
!user.data?.is_evaluator
|
||||
) {
|
||||
router.push({
|
||||
name: 'ProgrammingExerciseSubmissions',
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const getExerciseCount = () => {
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Programming Exercise',
|
||||
|
||||
@@ -19,4 +19,29 @@ type Filters = {
|
||||
exercise?: string,
|
||||
member?: 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user