fix: assignment submission issue

This commit is contained in:
Jannat Patel
2026-02-24 12:02:10 +05:30
parent b77c4867e1
commit bf19ebd3a8
8 changed files with 141 additions and 162 deletions

View File

@@ -64,8 +64,8 @@
import { Dialog, FormControl } from 'frappe-ui'
import { nextTick, onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { Link } from 'frappe-ui/frappe'
import { getLmsRoute } from '@/utils/basePath'
import Link from '@/components/Controls/Link.vue'
const show = ref(false)
const quiz = ref(null)

View File

@@ -16,8 +16,8 @@
{{ __('Submission by') }} {{ submissionResource.doc?.member_name }}
</div>
</div>
<div class="text-sm text-ink-gray-7 font-medium mb-2">
{{ __('Question') }}:
<div class="text-ink-gray-9 font-semibold mb-5">
{{ __('Assignment Question') }}
</div>
<div
v-html="assignment.data.question"
@@ -42,7 +42,11 @@
>
{{ submissionResource.doc?.status }}
</Badge>
<Button variant="solid" @click="submitAssignment()">
<Button
v-if="canModifyAssignment"
variant="solid"
@click="submitAssignment()"
>
{{ __('Save') }}
</Button>
</div>
@@ -73,12 +77,14 @@
}}
</div>
<FileUploader
v-if="!submissionResource.doc?.assignment_attachment"
v-if="!attachment"
:fileTypes="getType()"
:uploadArgs="{
private: true,
}"
:validateFile="validateFile"
:validateFile="
(file) => validateFile(file, assignment.data.type.toLowerCase())
"
@success="(file) => saveSubmission(file)"
>
<template #default="{ uploading, progress, openFileSelector }">
@@ -94,7 +100,7 @@
<div v-else>
<div class="flex items-center text-ink-gray-7">
<a
:href="submissionResource.doc.assignment_attachment"
:href="attachment"
target="_blank"
class="cursor-pointer !no-underline text-sm leading-5"
>
@@ -103,11 +109,7 @@
<FileText class="h-5 w-5 stroke-1.5" />
</div>
<span>
{{
submissionResource.doc.assignment_attachment
.split('/')
.pop()
}}
{{ attachment.split('/').pop() }}
</span>
</div>
</a>
@@ -138,6 +140,7 @@
@change="(val) => (answer = val)"
:editable="true"
:fixedMenu="true"
:readonly="!canModifyAssignment"
:uploadArgs="{
private: true,
}"
@@ -150,7 +153,7 @@
user.data?.name == submissionResource.doc?.owner &&
submissionResource.doc?.comments
"
class="mt-8 p-3 border rounded-lg"
class="mt-8 p-3 border rounded-lg bg-surface-gray-2"
>
<div class="text-ink-gray-5 mb-4">
{{ __('Comments by Evaluator') }}
@@ -213,8 +216,10 @@ import {
import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
import { FileText, X } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import { validateFile } from '@/utils'
const answer = ref(null)
const attachment = ref(null)
const comments = ref(null)
const router = useRouter()
const user = inject('$user')
@@ -264,118 +269,98 @@ const assignment = createResource({
},
})
const newSubmission = createResource({
url: 'frappe.client.insert',
makeParams(values) {
let doc = {
doctype: 'LMS Assignment Submission',
assignment: props.assignmentID,
member: user.data?.name,
}
if (!showUploader()) {
doc.answer = answer.value
}
return {
doc: doc,
}
},
})
const submissionResource = createDocumentResource({
doctype: 'LMS Assignment Submission',
name: props.submissionName,
auto: false,
onError(err) {
toast.error(err.messages?.[0] || err)
},
auto: false,
cache: [user.data?.name, props.assignmentID],
})
watch(submissionResource, () => {
if (submissionResource.doc) {
if (submissionResource.doc.answer) {
answer.value = submissionResource.doc.answer
}
if (submissionResource.doc.comments) {
comments.value = submissionResource.doc.comments
}
if (submissionResource.isDirty) {
isDirty.value = true
} else if (
showUploader() &&
!submissionResource.doc.assignment_attachment
) {
isDirty.value = true
} else if (!showUploader() && !answer.value) {
isDirty.value = true
} else {
isDirty.value = false
}
if (!submissionResource.doc) return
console.log(submissionResource.doc)
if (submissionResource.doc.answer) {
answer.value = submissionResource.doc.answer
}
if (submissionResource.doc.assignment_attachment) {
attachment.value = submissionResource.doc.assignment_attachment
}
if (submissionResource.doc.comments) {
comments.value = submissionResource.doc.comments
}
})
watch(
() => submissionResource.doc,
() => {
if (
props.submissionName == 'new' &&
submissionResource.doc?.assignment_attachment
) {
isDirty.value = true
}
}
)
const submitAssignment = () => {
if (props.submissionName != 'new') {
let evaluator =
submissionResource.doc && submissionResource.doc.owner != user.data?.name
? user.data?.name
: null
submissionResource.setValue.submit(
{
...submissionResource.doc,
evaluator: evaluator,
comments: comments.value,
answer: answer.value,
},
{
onSuccess(data) {
isDirty.value = false
toast.success(__('Changes saved successfully'))
},
}
)
updateSubmission()
} else {
addNewSubmission()
}
}
const addNewSubmission = () => {
newSubmission.submit(
{},
let doc = {
doctype: 'LMS Assignment Submission',
assignment: props.assignmentID,
member: user.data?.name,
}
if (!showUploader()) {
doc.answer = answer.value
} else {
doc.assignment_attachment = attachment.value
}
call('frappe.client.insert', {
doc: doc,
})
.then((data) => {
toast.success(__('Assignment submitted successfully'))
if (router.currentRoute.value.name == 'AssignmentSubmission') {
router.push({
name: 'AssignmentSubmission',
params: {
assignmentID: props.assignmentID,
submissionName: data.name,
},
query: { fromLesson: router.currentRoute.value.query.fromLesson },
})
} else {
markLessonProgress()
router.go()
}
isDirty.value = false
submissionResource.name = data.name
submissionResource.reload()
})
.catch((err) => {
toast.error(err.messages?.[0] || err)
console.error(err)
})
}
const updateSubmission = () => {
let evaluator =
submissionResource.doc && submissionResource.doc.owner != user.data?.name
? user.data?.name
: null
submissionResource.setValue.submit(
{
...submissionResource.doc,
evaluator: evaluator,
comments: comments.value,
answer: answer.value,
assignment_attachment: attachment.value,
},
{
onSuccess(data) {
toast.success(__('Assignment submitted successfully'))
if (router.currentRoute.value.name == 'AssignmentSubmission') {
router.push({
name: 'AssignmentSubmission',
params: {
assignmentID: props.assignmentID,
submissionName: data.name,
},
query: { fromLesson: router.currentRoute.value.query.fromLesson },
})
} else {
markLessonProgress()
router.go()
}
submissionResource.name = data.name
submissionResource.reload()
isDirty.value = false
toast.success(__('Changes saved successfully'))
},
onError(err) {
toast.error(err.messages?.[0] || err)
console.error(err)
},
}
)
@@ -383,7 +368,7 @@ const addNewSubmission = () => {
const saveSubmission = (file) => {
isDirty.value = true
submissionResource.doc.assignment_attachment = file.file_url
attachment.value = file.file_url
}
const markLessonProgress = () => {
@@ -417,21 +402,6 @@ const getType = () => {
}
}
const validateFile = (file) => {
let type = assignment.data?.type
let extension = file.name.split('.').pop().toLowerCase()
if (type == 'Image' && !['jpg', 'jpeg', 'png'].includes(extension)) {
return 'Only image file is allowed.'
} else if (
type == 'Document' &&
!['doc', 'docx', 'xml'].includes(extension)
) {
return 'Only document file is allowed.'
} else if (type == 'PDF' && !['pdf'].includes(extension)) {
return 'Only PDF file is allowed.'
}
}
const removeSubmission = () => {
isDirty.value = true
submissionResource.doc.assignment_attachment = ''

View File

@@ -111,44 +111,36 @@
</div>
<div class="px-5 pb-5 space-y-5 border-b mb-5">
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div class="space-y-5">
<FormControl
v-model="batchDetail.doc.description"
:label="__('Short Description')"
type="textarea"
:rows="4"
:placeholder="__('Short description of the batch')"
:required="true"
/>
<MultiSelect
v-model="instructors"
doctype="Course Evaluator"
:label="__('Instructors')"
:required="true"
:onCreate="(close) => openSettings('Evaluators', close)"
:filters="{ ignore_user_type: 1 }"
/>
<Uploader
v-model="batchDetail.doc.video_link"
:label="__('Preview Video')"
type="video"
:required="false"
/>
</div>
<div>
<label class="block text-sm text-ink-gray-5 mb-2">
{{ __('Batch Details') }}
<span class="text-ink-red-3">*</span>
</label>
<TextEditor
:content="batchDetail.doc.batch_details"
@change="(val) => (batchDetail.doc.batch_details = 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-[16rem] overflow-y-scroll mb-4"
/>
</div>
<div class="grid grid-cols-2 gap-5">
<MultiSelect
v-model="instructors"
doctype="Course Evaluator"
:label="__('Instructors')"
:required="true"
:onCreate="(close) => openSettings('Evaluators', close)"
:filters="{ ignore_user_type: 1 }"
/>
<FormControl
v-model="batchDetail.doc.description"
:label="__('Short Description')"
type="textarea"
:rows="4"
:placeholder="__('Short description of the batch')"
:required="true"
/>
</div>
<div>
<label class="block text-sm text-ink-gray-5 mb-2">
{{ __('Batch Details') }}
<span class="text-ink-red-3">*</span>
</label>
<TextEditor
:content="batchDetail.doc.batch_details"
@change="(val) => (batchDetail.doc.batch_details = 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-[16rem] overflow-y-scroll mb-4"
/>
</div>
</div>
@@ -184,6 +176,12 @@
}
"
/>
<Uploader
v-model="batchDetail.doc.video_link"
:label="__('Preview Video')"
type="video"
:required="false"
/>
</div>
</div>
</div>

View File

@@ -32,7 +32,7 @@
v-model="batch.category"
:label="__('Category')"
:allowCreate="true"
@create="
:onCreate="
() => {
openSettings('Categories')
show = false
@@ -110,10 +110,11 @@
</template>
<script setup lang="ts">
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { Link, useOnboarding, useTelemetry } from 'frappe-ui/frappe'
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
import { ref, inject, onMounted, onBeforeUnmount, watch } from 'vue'
import { useRouter } from 'vue-router'
import { cleanError, openSettings } from '@/utils'
import Link from '@/components/Controls/Link.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
const show = defineModel<boolean>({ required: true, default: false })

View File

@@ -80,7 +80,7 @@
<div class="pr-5 md:pr-10 pb-5 mb-5 space-y-5 border-b">
<div class="text-lg font-semibold text-ink-gray-9">
{{ __('Settings') }}
{{ __('Publishing Settings') }}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div

View File

@@ -61,7 +61,7 @@
<div class="grid grid-cols-2 gap-2">
<FormControl
v-model="title"
:placeholder="__('Search by Title')"
:placeholder="__('Search')"
type="text"
class="w-full lg:min-w-0 lg:w-32 xl:w-40"
@input="updateCourses()"

View File

@@ -18,8 +18,7 @@
doctype="LMS Category"
v-model="course.category"
:label="__('Category')"
:allowCreate="true"
@create="
:onCreate="
() => {
openSettings('Categories')
show = false
@@ -75,10 +74,11 @@
</template>
<script setup lang="ts">
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
import { Link, useOnboarding, useTelemetry } from 'frappe-ui/frappe'
import { useOnboarding, useTelemetry } from 'frappe-ui/frappe'
import { inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
import { useRouter } from 'vue-router'
import { cleanError, openSettings } from '@/utils'
import Link from '@/components/Controls/Link.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Uploader from '@/components/Controls/Uploader.vue'

View File

@@ -644,6 +644,7 @@ export const validateFile = async (
showToast = true,
fileType = 'image'
) => {
const extension = file.name.split('.').pop().toLowerCase()
const error = (msg) => {
if (showToast) toast.error(msg)
console.error(msg)
@@ -653,6 +654,16 @@ export const validateFile = async (
return error(__('Only {0} file is allowed.').format(fileType))
}
if (fileType == 'pdf' && extension !== 'pdf') {
return error(__('Only PDF files are allowed.'))
}
if (fileType == 'document' && !['doc', 'docx'].includes(extension)) {
return error(
__('Only document file of type .doc or .docx are allowed.')
)
}
if (file.type === 'image/svg+xml') {
const text = await file.text()
@@ -680,7 +691,6 @@ export const validateFile = async (
export const escapeHTML = (text) => {
if (!text) return ''
let escape_html_mapping = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',