feat: assignment grading
This commit is contained in:
@@ -41,6 +41,11 @@
|
|||||||
<div v-if="column.key == 'assessment_type'">
|
<div v-if="column.key == 'assessment_type'">
|
||||||
{{ row[column.key] == 'LMS Quiz' ? 'Quiz' : 'Assignment' }}
|
{{ row[column.key] == 'LMS Quiz' ? 'Quiz' : 'Assignment' }}
|
||||||
</div>
|
</div>
|
||||||
|
<div v-else-if="column.key == 'status'">
|
||||||
|
<Badge :theme="getStatusTheme(row[column.key])">
|
||||||
|
{{ row[column.key] }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
{{ row[column.key] }}
|
{{ row[column.key] }}
|
||||||
</div>
|
</div>
|
||||||
@@ -83,6 +88,7 @@ import {
|
|||||||
ListSelectBanner,
|
ListSelectBanner,
|
||||||
createResource,
|
createResource,
|
||||||
Button,
|
Button,
|
||||||
|
Badge,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { inject, ref } from 'vue'
|
import { inject, ref } from 'vue'
|
||||||
import AssessmentModal from '@/components/Modals/AssessmentModal.vue'
|
import AssessmentModal from '@/components/Modals/AssessmentModal.vue'
|
||||||
@@ -180,7 +186,7 @@ const getAssessmentColumns = () => {
|
|||||||
{
|
{
|
||||||
label: 'Assessment',
|
label: 'Assessment',
|
||||||
key: 'title',
|
key: 'title',
|
||||||
width: '30rem',
|
width: '25rem',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Type',
|
label: 'Type',
|
||||||
@@ -199,4 +205,14 @@ const getAssessmentColumns = () => {
|
|||||||
}
|
}
|
||||||
return columns
|
return columns
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const getStatusTheme = (status) => {
|
||||||
|
if (status === 'Pass') {
|
||||||
|
return 'green'
|
||||||
|
} else if (status === 'Not Graded') {
|
||||||
|
return 'orange'
|
||||||
|
} else {
|
||||||
|
return 'red'
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -29,8 +29,8 @@
|
|||||||
<slot name="item-label" v-bind="{ active, selected, option }" />
|
<slot name="item-label" v-bind="{ active, selected, option }" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-if="attrs.onCreate" #footer="{ value, close }">
|
<template #footer="{ value, close }">
|
||||||
<div>
|
<div v-if="attrs.onCreate">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
class="w-full !justify-start"
|
class="w-full !justify-start"
|
||||||
@@ -42,6 +42,18 @@
|
|||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
class="w-full !justify-start"
|
||||||
|
:label="__('Clear')"
|
||||||
|
@click="() => clearValue(close)"
|
||||||
|
>
|
||||||
|
<template #prefix>
|
||||||
|
<X class="h-4 w-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Autocomplete>
|
</Autocomplete>
|
||||||
<p v-if="description" class="text-sm text-gray-600">{{ description }}</p>
|
<p v-if="description" class="text-sm text-gray-600">{{ description }}</p>
|
||||||
@@ -52,7 +64,7 @@
|
|||||||
import Autocomplete from '@/components/Controls/Autocomplete.vue'
|
import Autocomplete from '@/components/Controls/Autocomplete.vue'
|
||||||
import { watchDebounced } from '@vueuse/core'
|
import { watchDebounced } from '@vueuse/core'
|
||||||
import { createResource, Button } from 'frappe-ui'
|
import { createResource, Button } from 'frappe-ui'
|
||||||
import { Plus } from 'lucide-vue-next'
|
import { Plus, X } from 'lucide-vue-next'
|
||||||
import { useAttrs, computed, ref } from 'vue'
|
import { useAttrs, computed, ref } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -75,9 +87,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'change'])
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
const attrs = useAttrs()
|
const attrs = useAttrs()
|
||||||
|
|
||||||
const valuePropPassed = computed(() => 'value' in attrs)
|
const valuePropPassed = computed(() => 'value' in attrs)
|
||||||
|
|
||||||
const value = computed({
|
const value = computed({
|
||||||
@@ -131,7 +141,7 @@ const options = createResource({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
function reload(val) {
|
const reload = (val) => {
|
||||||
options.update({
|
options.update({
|
||||||
params: {
|
params: {
|
||||||
txt: val,
|
txt: val,
|
||||||
@@ -142,6 +152,11 @@ function reload(val) {
|
|||||||
options.reload()
|
options.reload()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const clearValue = (close) => {
|
||||||
|
emit(valuePropPassed.value ? 'change' : 'update:modelValue', '')
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
|
||||||
const labelClasses = computed(() => {
|
const labelClasses = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
v-if="assignment.doc?.name"
|
v-if="assignment.doc?.name"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'AssignmentSubmissionList',
|
name: 'AssignmentSubmissionList',
|
||||||
params: {
|
query: {
|
||||||
assignmentID: assignment.doc.name,
|
assignmentID: assignment.doc.name,
|
||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
|
|||||||
@@ -3,110 +3,128 @@
|
|||||||
class="flex justify-between sticky top-0 z-10 border-b bg-white px-3 py-2.5 sm:px-5"
|
class="flex justify-between sticky top-0 z-10 border-b bg-white px-3 py-2.5 sm:px-5"
|
||||||
>
|
>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
<Button variant="solid" @click="submitAssignment()">
|
<div class="flex items-center space-x-2">
|
||||||
{{ __('Save') }}
|
<Badge
|
||||||
</Button>
|
v-if="submissionResource.doc?.status"
|
||||||
</header>
|
:theme="statusTheme"
|
||||||
<div class="container py-5">
|
size="lg"
|
||||||
<div
|
>
|
||||||
v-if="submissionResource.data"
|
{{ submissionResource.doc.status }}
|
||||||
class="bg-blue-100 p-2 rounded-md leading-5 text-sm mb-4"
|
</Badge>
|
||||||
>
|
<Button variant="solid" @click="submitAssignment()">
|
||||||
{{ __("You've successfully submitted the assignment.") }}
|
{{ __('Save') }}
|
||||||
{{
|
</Button>
|
||||||
__(
|
|
||||||
"Once the moderator grades your submission, you'll find the details here."
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
{{ __('Feel free to make edits to your submission if needed.') }}
|
|
||||||
</div>
|
</div>
|
||||||
<div v-if="assignment.data">
|
</header>
|
||||||
<div>
|
<div class="p-5 overflow-hidden h-[calc(100vh-3.2rem)]">
|
||||||
<div class="text-xl font-semibold hidden">
|
<div v-if="assignment.data" class="grid grid-cols-2 gap-5">
|
||||||
|
<div class="p-5 border rounded-md overflow-y-auto h-[calc(100vh-6rem)]">
|
||||||
|
<div class="text-sm text-gray-600 font-medium mb-2">
|
||||||
{{ __('Question') }}
|
{{ __('Question') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-sm mt-1 hidden">
|
|
||||||
{{
|
|
||||||
__('Read the question carefully before attempting the assignment.')
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
v-html="assignment.data.question"
|
v-html="assignment.data.question"
|
||||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal"
|
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-gray-300 prose-th:border-gray-300 prose-td:relative prose-th:relative prose-th:bg-gray-100 prose-sm max-w-none !whitespace-normal"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="">
|
|
||||||
<div class="text-xl font-semibold mt-10">
|
<div class="flex flex-col">
|
||||||
{{ __('Submission') }}
|
<div class="p-5 border rounded-md">
|
||||||
</div>
|
<div class="text-sm text-gray-600 font-medium mb-4">
|
||||||
<div v-if="showUploader()">
|
{{ __('Submission') }}
|
||||||
<div class="text-sm mt-1 mb-4">
|
|
||||||
{{ __('Add your assignment as {0}').format(assignment.data.type) }}
|
|
||||||
</div>
|
</div>
|
||||||
<FileUploader
|
<div
|
||||||
v-if="!submissionFile"
|
v-if="submissionName != 'new'"
|
||||||
:fileTypes="getType()"
|
class="bg-blue-100 p-2 rounded-md leading-5 text-sm mb-4"
|
||||||
:validateFile="validateFile"
|
|
||||||
@success="(file) => saveSubmission(file)"
|
|
||||||
>
|
>
|
||||||
<template
|
{{ __("You've successfully submitted the assignment.") }}
|
||||||
#default="{
|
{{
|
||||||
file,
|
__(
|
||||||
uploading,
|
"Once the moderator grades your submission, you'll find the details here."
|
||||||
progress,
|
)
|
||||||
uploaded,
|
}}
|
||||||
message,
|
{{ __('Feel free to make edits to your submission if needed.') }}
|
||||||
error,
|
</div>
|
||||||
total,
|
<div v-if="showUploader()">
|
||||||
success,
|
<div class="text-xs text-gray-600 mt-1 mb-2">
|
||||||
openFileSelector,
|
{{
|
||||||
}"
|
__('Add your assignment as {0}').format(assignment.data.type)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<FileUploader
|
||||||
|
v-if="!submissionFile"
|
||||||
|
:fileTypes="getType()"
|
||||||
|
:validateFile="validateFile"
|
||||||
|
@success="(file) => saveSubmission(file)"
|
||||||
>
|
>
|
||||||
<Button @click="openFileSelector" :loading="uploading">
|
<template #default="{ uploading, progress, openFileSelector }">
|
||||||
{{
|
<Button @click="openFileSelector" :loading="uploading">
|
||||||
uploading
|
{{
|
||||||
? __('Uploading {0}%').format(progress)
|
uploading
|
||||||
: __('Upload File')
|
? __('Uploading {0}%').format(progress)
|
||||||
}}
|
: __('Upload File')
|
||||||
</Button>
|
}}
|
||||||
</template>
|
</Button>
|
||||||
</FileUploader>
|
</template>
|
||||||
<div v-else>
|
</FileUploader>
|
||||||
<div class="flex items-center">
|
<div v-else>
|
||||||
<div class="border rounded-md p-2 mr-2">
|
<div class="flex items-center">
|
||||||
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" />
|
<div class="border rounded-md p-2 mr-2">
|
||||||
|
<FileText class="h-5 w-5 stroke-1.5 text-gray-700" />
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span>
|
||||||
|
{{ submissionFile.file_name }}
|
||||||
|
</span>
|
||||||
|
<span class="text-sm text-gray-500 mt-1">
|
||||||
|
{{ getFileSize(submissionFile.file_size) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<X
|
||||||
|
v-if="submissionResource.doc.owner == user.data?.name"
|
||||||
|
@click="removeSubmission()"
|
||||||
|
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
|
||||||
<span>
|
|
||||||
{{ submissionFile.file_name }}
|
|
||||||
</span>
|
|
||||||
<span class="text-sm text-gray-500 mt-1">
|
|
||||||
{{ getFileSize(submissionFile.file_size) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<X
|
|
||||||
@click="removeSubmission()"
|
|
||||||
class="bg-gray-200 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-else-if="assignment.data.type == 'URL'" class="mt-5">
|
||||||
<div v-else-if="assignment.data.type == 'URL'">
|
<div class="text-xs text-gray-600 mb-1">
|
||||||
<div class="text-sm mb-4">
|
{{ __('Enter a URL') }}
|
||||||
{{ __('Enter a URL') }}
|
</div>
|
||||||
|
<FormControl v-model="answer" />
|
||||||
</div>
|
</div>
|
||||||
<FormControl v-model="answer" />
|
<div v-else>
|
||||||
</div>
|
<div class="text-sm mb-4">
|
||||||
<div v-else>
|
{{ __('Write your answer here') }}
|
||||||
<div class="text-sm mb-4">
|
</div>
|
||||||
{{ __('Write your answer here') }}
|
<TextEditor
|
||||||
|
:content="answer"
|
||||||
|
@change="(val) => (answer = val)"
|
||||||
|
:editable="true"
|
||||||
|
:fixedMenu="true"
|
||||||
|
editorClass="prose-sm max-w-none border-b border-x bg-gray-100 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TextEditor
|
</div>
|
||||||
:content="answer"
|
<!-- Grading -->
|
||||||
@change="(val) => (answer = val)"
|
<div
|
||||||
:editable="true"
|
v-if="canGradeSubmission"
|
||||||
:fixedMenu="true"
|
class="p-3 border rounded-md mt-5 space-y-4"
|
||||||
editorClass="prose-sm max-w-none border-b border-x bg-gray-100 rounded-b-md py-1 px-2 min-h-[7rem]"
|
>
|
||||||
|
<div class="text-sm text-gray-600 font-medium mb-5">
|
||||||
|
{{ __('Grading') }}
|
||||||
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-model="submissionResource.doc.status"
|
||||||
|
:label="__('Grade')"
|
||||||
|
type="select"
|
||||||
|
:options="submissionStatusOptions"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="submissionResource.doc.comments"
|
||||||
|
:label="__('Comments')"
|
||||||
|
type="textarea"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -119,8 +137,10 @@ import {
|
|||||||
createResource,
|
createResource,
|
||||||
FileUploader,
|
FileUploader,
|
||||||
Button,
|
Button,
|
||||||
|
Badge,
|
||||||
FormControl,
|
FormControl,
|
||||||
TextEditor,
|
TextEditor,
|
||||||
|
createDocumentResource,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { FileText, X } from 'lucide-vue-next'
|
import { FileText, X } from 'lucide-vue-next'
|
||||||
import { computed, inject, onMounted, ref } from 'vue'
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
@@ -143,6 +163,12 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!user.data) {
|
||||||
|
window.location.href = '/login'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const assignment = createResource({
|
const assignment = createResource({
|
||||||
url: 'frappe.client.get',
|
url: 'frappe.client.get',
|
||||||
params: {
|
params: {
|
||||||
@@ -150,29 +176,17 @@ const assignment = createResource({
|
|||||||
name: props.assignmentID,
|
name: props.assignmentID,
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
|
onSuccess(data) {
|
||||||
|
if (props.submissionName != 'new') {
|
||||||
|
submissionResource.reload()
|
||||||
|
}
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const showUploader = () => {
|
const showUploader = () => {
|
||||||
return ['PDF', 'Image', 'Document'].includes(assignment.data?.type)
|
return ['PDF', 'Image', 'Document'].includes(assignment.data?.type)
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateSubmission = createResource({
|
|
||||||
url: 'frappe.client.set_value',
|
|
||||||
makeParams(values) {
|
|
||||||
let fieldname = {}
|
|
||||||
if (showUploader()) {
|
|
||||||
fieldname.assignment_attachment = submissionFile.value.file_url
|
|
||||||
} else {
|
|
||||||
fieldname.answer = answer.value
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
doctype: 'LMS Assignment Submission',
|
|
||||||
name: props.submissionName,
|
|
||||||
fieldname: fieldname,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const imageResource = createResource({
|
const imageResource = createResource({
|
||||||
url: 'lms.lms.api.get_file_info',
|
url: 'lms.lms.api.get_file_info',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
@@ -205,43 +219,25 @@ const newSubmission = createResource({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const submissionResource = createResource({
|
const submissionResource = createDocumentResource({
|
||||||
url: 'frappe.client.get_value',
|
doctype: 'LMS Assignment Submission',
|
||||||
params: {
|
name: props.submissionName,
|
||||||
doctype: 'LMS Assignment Submission',
|
auto: false,
|
||||||
fieldname: showUploader() ? 'assignment_attachment' : 'answer',
|
|
||||||
filters: {
|
|
||||||
name: props.submissionName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
if (data.assignment_attachment)
|
if (data.assignment_attachment) {
|
||||||
imageResource.reload({ image: data.assignment_attachment })
|
imageResource.reload({ image: data.assignment_attachment })
|
||||||
if (data.answer) answer.value = data.answer
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
onMounted(() => {
|
|
||||||
if (!user.data) {
|
|
||||||
window.location.href = '/login'
|
|
||||||
}
|
|
||||||
if (props.submissionName != 'new') {
|
|
||||||
submissionResource.reload()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const submitAssignment = () => {
|
const submitAssignment = () => {
|
||||||
if (props.submissionName != 'new') {
|
if (props.submissionName != 'new') {
|
||||||
updateSubmission.submit(
|
let evaluator =
|
||||||
{},
|
submissionResource.doc.owner != user.data?.name ? user.data?.name : null
|
||||||
{
|
submissionResource.setValue.submit({
|
||||||
onSuccess(data) {
|
...submissionResource.doc,
|
||||||
showToast('Success', 'Submission updated successfully.', 'check')
|
evaluator: evaluator,
|
||||||
},
|
})
|
||||||
onError(err) {
|
|
||||||
showToast('Error', err.messages?.[0] || err, 'x')
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
addNewSubmission()
|
addNewSubmission()
|
||||||
}
|
}
|
||||||
@@ -271,7 +267,8 @@ const addNewSubmission = () => {
|
|||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
let crumbs = [
|
let crumbs = [
|
||||||
{
|
{
|
||||||
label: 'Assignment',
|
label: 'Submissions',
|
||||||
|
route: { name: 'AssignmentSubmissionList' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: assignment.data?.title,
|
label: assignment.data?.title,
|
||||||
@@ -325,4 +322,31 @@ const validateFile = (file) => {
|
|||||||
const removeSubmission = () => {
|
const removeSubmission = () => {
|
||||||
submissionFile.value = null
|
submissionFile.value = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const canGradeSubmission = computed(() => {
|
||||||
|
return (
|
||||||
|
(user.data?.is_moderator ||
|
||||||
|
user.data?.is_evaluator ||
|
||||||
|
user.data?.is_instructor) &&
|
||||||
|
props.submissionName != 'new'
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const submissionStatusOptions = computed(() => {
|
||||||
|
return [
|
||||||
|
{ label: 'Not Graded', value: 'Not Graded' },
|
||||||
|
{ label: 'Pass', value: 'Pass' },
|
||||||
|
{ label: 'Fail', value: 'Fail' },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const statusTheme = computed(() => {
|
||||||
|
if (submissionResource.doc.status == 'Pass') {
|
||||||
|
return 'green'
|
||||||
|
} else if (submissionResource.doc.status == 'Not Graded') {
|
||||||
|
return 'orange'
|
||||||
|
} else {
|
||||||
|
return 'red'
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,19 +4,23 @@
|
|||||||
>
|
>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</header>
|
</header>
|
||||||
<div
|
<div class="md:w-3/4 md:mx-auto py-5 mx-5">
|
||||||
v-if="submissions.loading || submissions.data?.length"
|
<div class="grid grid-cols-3 gap-5 mb-5">
|
||||||
class="md:w-3/4 md:mx-auto py-5 mx-5"
|
|
||||||
>
|
|
||||||
<div class="grid grid-cols-4 gap-5 mb-5">
|
|
||||||
<Link
|
<Link
|
||||||
doctype="LMS Assignment"
|
doctype="LMS Assignment"
|
||||||
v-model="assignmentID"
|
v-model="assignmentID"
|
||||||
:placeholder="__('Assignment')"
|
:placeholder="__('Assignment')"
|
||||||
/>
|
/>
|
||||||
<Link doctype="User" v-model="member" :placeholder="__('Member')" />
|
<Link doctype="User" v-model="member" :placeholder="__('Member')" />
|
||||||
|
<FormControl
|
||||||
|
v-model="status"
|
||||||
|
type="select"
|
||||||
|
:options="statusOptions"
|
||||||
|
:placeholder="__('Status')"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ListView
|
<ListView
|
||||||
|
v-if="submissions.loading || submissions.data?.length"
|
||||||
:columns="submissionColumns"
|
:columns="submissionColumns"
|
||||||
:rows="submissions.data"
|
:rows="submissions.data"
|
||||||
rowKey="name"
|
rowKey="name"
|
||||||
@@ -54,22 +58,18 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</ListRows>
|
</ListRows>
|
||||||
</ListView>
|
</ListView>
|
||||||
</div>
|
<div
|
||||||
<div
|
v-else
|
||||||
v-else
|
class="text-center p-5 text-gray-600 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
||||||
class="text-center p-5 text-gray-600 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
>
|
||||||
>
|
<Pencil class="size-8 mx-auto stroke-1 text-gray-500" />
|
||||||
<Pencil class="size-8 mx-auto stroke-1 text-gray-500" />
|
<div class="text-xl font-medium">
|
||||||
<div class="text-xl font-medium">
|
{{ __('No submissions') }}
|
||||||
{{ __('No submissions') }}
|
</div>
|
||||||
|
<div class="leading-5">
|
||||||
|
{{ __('There are no submissions for this assignment.') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- <div class="leading-5">
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'There are no submissions for the assignment {0}.',
|
|
||||||
).format(assignmentTitle.data?.title)
|
|
||||||
}}
|
|
||||||
</div> -->
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
@@ -77,7 +77,7 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
createListResource,
|
createListResource,
|
||||||
createResource,
|
FormControl,
|
||||||
ListView,
|
ListView,
|
||||||
ListHeader,
|
ListHeader,
|
||||||
ListHeaderItem,
|
ListHeaderItem,
|
||||||
@@ -95,33 +95,34 @@ const dayjs = inject('$dayjs')
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const assignmentID = ref('')
|
const assignmentID = ref('')
|
||||||
const member = ref('')
|
const member = ref('')
|
||||||
|
const status = ref('')
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!user.data?.is_instructor && !user.data?.is_moderator) {
|
if (!user.data?.is_instructor && !user.data?.is_moderator) {
|
||||||
router.push({ name: 'Courses' })
|
router.push({ name: 'Courses' })
|
||||||
}
|
}
|
||||||
|
assignmentID.value = router.currentRoute.value.query.assignmentID
|
||||||
assignmentID.value = router.currentRoute.value.params.assignmentID
|
member.value = router.currentRoute.value.query.member
|
||||||
submissions.reload()
|
status.value = router.currentRoute.value.query.status
|
||||||
|
reloadSubmissions()
|
||||||
})
|
})
|
||||||
|
|
||||||
const getAssignmentFilters = () => {
|
const getAssignmentFilters = () => {
|
||||||
let filters = {}
|
let filters = {}
|
||||||
if (assignmentID.value) {
|
if (assignmentID.value) {
|
||||||
console.log(assignmentID.value)
|
|
||||||
filters.assignment = assignmentID.value
|
filters.assignment = assignmentID.value
|
||||||
}
|
}
|
||||||
if (member.value) {
|
if (member.value) {
|
||||||
console.log(member.value)
|
|
||||||
filters.member = member.value
|
filters.member = member.value
|
||||||
}
|
}
|
||||||
console.log(filters)
|
if (status.value) {
|
||||||
|
filters.status = status.value
|
||||||
|
}
|
||||||
return filters
|
return filters
|
||||||
}
|
}
|
||||||
|
|
||||||
const submissions = createListResource({
|
const submissions = createListResource({
|
||||||
doctype: 'LMS Assignment Submission',
|
doctype: 'LMS Assignment Submission',
|
||||||
filters: getAssignmentFilters(),
|
|
||||||
fields: [
|
fields: [
|
||||||
'name',
|
'name',
|
||||||
'assignment',
|
'assignment',
|
||||||
@@ -141,26 +142,31 @@ const submissions = createListResource({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
watch([assignmentID, member], () => {
|
// watch changes in assignmentID, member, and status and if changes in any then reload submissions. Also update the url query params for the same
|
||||||
console.log('watch called')
|
watch([assignmentID, member, status], () => {
|
||||||
submissions.reload()
|
router.push({
|
||||||
|
query: {
|
||||||
|
assignmentID: assignmentID.value,
|
||||||
|
member: member.value,
|
||||||
|
status: status.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
reloadSubmissions()
|
||||||
})
|
})
|
||||||
|
|
||||||
/* const assignmentTitle = createResource({
|
const reloadSubmissions = () => {
|
||||||
url: "frappe.client.get_value",
|
submissions.update({
|
||||||
params: {
|
filters: getAssignmentFilters(),
|
||||||
doctype: "LMS Assignment",
|
})
|
||||||
fieldname: "title",
|
submissions.reload()
|
||||||
filters: { name: props.assignmentID },
|
}
|
||||||
},
|
|
||||||
}) */
|
|
||||||
|
|
||||||
const submissionColumns = computed(() => {
|
const submissionColumns = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Member',
|
label: 'Member',
|
||||||
key: 'member_name',
|
key: 'member_name',
|
||||||
width: 2,
|
width: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Assignment',
|
label: 'Assignment',
|
||||||
@@ -182,6 +188,15 @@ const submissionColumns = computed(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const statusOptions = computed(() => {
|
||||||
|
return [
|
||||||
|
{ label: '', value: '' },
|
||||||
|
{ label: 'Pass', value: 'Pass' },
|
||||||
|
{ label: 'Fail', value: 'Fail' },
|
||||||
|
{ label: 'Not Graded', value: 'Not Graded' },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
const getStatusTheme = (status) => {
|
const getStatusTheme = (status) => {
|
||||||
if (status === 'Pass') {
|
if (status === 'Pass') {
|
||||||
return 'green'
|
return 'green'
|
||||||
|
|||||||
@@ -20,8 +20,18 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div v-if="assignments.data?.length" class="md:w-3/4 md:mx-auto py-5 mx-5">
|
<div class="md:w-3/4 md:mx-auto py-5 mx-5">
|
||||||
|
<div class="grid grid-cols-3 gap-5 mb-5">
|
||||||
|
<FormControl v-model="titleFilter" :placeholder="__('Search by title')" />
|
||||||
|
<FormControl
|
||||||
|
v-model="typeFilter"
|
||||||
|
type="select"
|
||||||
|
:options="assignmentTypes"
|
||||||
|
:placeholder="__('Type')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<ListView
|
<ListView
|
||||||
|
v-if="assignments.data?.length"
|
||||||
:columns="assignmentColumns"
|
:columns="assignmentColumns"
|
||||||
:rows="assignments.data"
|
:rows="assignments.data"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
@@ -37,50 +47,91 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
</ListView>
|
</ListView>
|
||||||
<div class="flex justify-center my-5">
|
<div
|
||||||
<Button v-if="assignments.hasNextPage" @click="assignments.next()">
|
v-else
|
||||||
|
class="text-center p-5 text-gray-600 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
||||||
|
>
|
||||||
|
<Pencil class="size-10 mx-auto stroke-1 text-gray-500" />
|
||||||
|
<div class="text-xl font-medium">
|
||||||
|
{{ __('No assignments found') }}
|
||||||
|
</div>
|
||||||
|
<div class="leading-5">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'You have not created any assignments yet. To create a new assignment, click on the "New" button above.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="assignments.hasNextPage" class="flex justify-center my-5">
|
||||||
|
<Button @click="assignments.next()">
|
||||||
{{ __('Load More') }}
|
{{ __('Load More') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="text-center p-5 text-gray-600 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
|
||||||
>
|
|
||||||
<Pencil class="size-10 mx-auto stroke-1 text-gray-500" />
|
|
||||||
<div class="text-xl font-medium">
|
|
||||||
{{ __('No assignments found') }}
|
|
||||||
</div>
|
|
||||||
<div class="leading-5">
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'You have not created any assignments yet. To create a new assignment, click on the "New" button above.'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Breadcrumbs, Button, createListResource, ListView } from 'frappe-ui'
|
import {
|
||||||
import { computed, inject } from 'vue'
|
Breadcrumbs,
|
||||||
|
Button,
|
||||||
|
createListResource,
|
||||||
|
FormControl,
|
||||||
|
ListView,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||||
import { Plus, Pencil } from 'lucide-vue-next'
|
import { Plus, Pencil } from 'lucide-vue-next'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
|
const titleFilter = ref('')
|
||||||
|
const typeFilter = ref('')
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!user.data?.is_moderator && !user.data?.is_instructor) {
|
||||||
|
router.push({ name: 'Courses' })
|
||||||
|
}
|
||||||
|
|
||||||
|
titleFilter.value = router.currentRoute.value.query.title
|
||||||
|
typeFilter.value = router.currentRoute.value.query.type
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([titleFilter, typeFilter], () => {
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
title: titleFilter.value,
|
||||||
|
type: typeFilter.value,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
reloadAssignments()
|
||||||
|
})
|
||||||
|
|
||||||
|
const reloadAssignments = () => {
|
||||||
|
assignments.update({
|
||||||
|
filters: assignmentFilter.value,
|
||||||
|
})
|
||||||
|
assignments.reload()
|
||||||
|
}
|
||||||
|
|
||||||
const assignmentFilter = computed(() => {
|
const assignmentFilter = computed(() => {
|
||||||
if (user.data?.is_moderator) return {}
|
let filters = {}
|
||||||
return {
|
if (titleFilter.value) {
|
||||||
owner: user.data?.name,
|
filters.title = ['like', `%${titleFilter.value}%`]
|
||||||
}
|
}
|
||||||
|
if (typeFilter.value) {
|
||||||
|
filters.type = typeFilter.value
|
||||||
|
}
|
||||||
|
if (!user.data?.is_moderator) {
|
||||||
|
filters.owner = user.data?.email
|
||||||
|
}
|
||||||
|
return filters
|
||||||
})
|
})
|
||||||
|
|
||||||
const assignments = createListResource({
|
const assignments = createListResource({
|
||||||
doctype: 'LMS Assignment',
|
doctype: 'LMS Assignment',
|
||||||
fields: ['name', 'title', 'type', 'creation'],
|
fields: ['name', 'title', 'type', 'creation'],
|
||||||
filters: assignmentFilter,
|
|
||||||
orderBy: 'modified desc',
|
orderBy: 'modified desc',
|
||||||
auto: true,
|
|
||||||
cache: ['assignments'],
|
cache: ['assignments'],
|
||||||
transform(data) {
|
transform(data) {
|
||||||
return data.map((row) => {
|
return data.map((row) => {
|
||||||
@@ -103,7 +154,7 @@ const assignmentColumns = computed(() => {
|
|||||||
label: __('Type'),
|
label: __('Type'),
|
||||||
key: 'type',
|
key: 'type',
|
||||||
width: 1,
|
width: 1,
|
||||||
align: 'center',
|
align: 'left',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: __('Created'),
|
label: __('Created'),
|
||||||
@@ -114,6 +165,16 @@ const assignmentColumns = computed(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const assignmentTypes = computed(() => {
|
||||||
|
let types = ['', 'Document', 'Image', 'PDF', 'URL', 'Text']
|
||||||
|
return types.map((type) => {
|
||||||
|
return {
|
||||||
|
label: __(type),
|
||||||
|
value: type,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
const breadcrumbs = computed(() => [
|
const breadcrumbs = computed(() => [
|
||||||
{
|
{
|
||||||
label: 'Assignments',
|
label: 'Assignments',
|
||||||
|
|||||||
Reference in New Issue
Block a user