fix: assignment submission issue
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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 = ''
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()"
|
||||
|
||||
@@ -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'
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
|
||||
Reference in New Issue
Block a user