feat: assignments list and form
This commit is contained in:
@@ -185,6 +185,17 @@ const addQuizzes = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addAssignments = () => {
|
||||||
|
if (isInstructor.value || isModerator.value) {
|
||||||
|
sidebarLinks.value.push({
|
||||||
|
label: 'Assignments',
|
||||||
|
icon: 'Pencil',
|
||||||
|
to: 'Assignments',
|
||||||
|
activeFor: ['Assignments', 'AssignmentForm'],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const addPrograms = () => {
|
const addPrograms = () => {
|
||||||
let activeFor = ['Programs', 'ProgramForm']
|
let activeFor = ['Programs', 'ProgramForm']
|
||||||
let index = 1
|
let index = 1
|
||||||
@@ -247,8 +258,9 @@ watch(userResource, () => {
|
|||||||
if (userResource.data) {
|
if (userResource.data) {
|
||||||
isModerator.value = userResource.data.is_moderator
|
isModerator.value = userResource.data.is_moderator
|
||||||
isInstructor.value = userResource.data.is_instructor
|
isInstructor.value = userResource.data.is_instructor
|
||||||
addQuizzes()
|
|
||||||
addPrograms()
|
addPrograms()
|
||||||
|
addQuizzes()
|
||||||
|
addAssignments()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
75
frontend/src/components/AssessmentPlugin.vue
Normal file
75
frontend/src/components/AssessmentPlugin.vue
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
size: 'xl',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body>
|
||||||
|
<div class="p-5 space-y-4">
|
||||||
|
<div v-if="type == 'quiz'" class="text-lg font-semibold">
|
||||||
|
{{ __('Add a quiz to your lesson') }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-lg font-semibold">
|
||||||
|
{{ __('Add an assignment to your lesson') }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Link
|
||||||
|
v-if="type == 'quiz'"
|
||||||
|
v-model="quiz"
|
||||||
|
doctype="LMS Quiz"
|
||||||
|
:label="__('Select a quiz')"
|
||||||
|
:onCreate="(value, close) => redirectToForm()"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
v-else
|
||||||
|
v-model="assignment"
|
||||||
|
doctype="LMS Assignment"
|
||||||
|
:label="__('Select an assignment')"
|
||||||
|
:onCreate="(value, close) => redirectToForm()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-2">
|
||||||
|
<Button variant="solid" @click="addAssessment()">
|
||||||
|
{{ __('Save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { Dialog, Button } from 'frappe-ui'
|
||||||
|
import { onMounted, ref, nextTick } from 'vue'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
|
const show = ref(false)
|
||||||
|
const quiz = ref(null)
|
||||||
|
const assignment = ref(null)
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
onAddition: {
|
||||||
|
type: Function,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await nextTick()
|
||||||
|
show.value = true
|
||||||
|
})
|
||||||
|
|
||||||
|
const addAssessment = () => {
|
||||||
|
props.onAddition(props.type == 'quiz' ? quiz.value : assignment.value)
|
||||||
|
show.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectToForm = () => {
|
||||||
|
if (props.type == 'quiz') window.open('/lms/quizzes/new', '_blank')
|
||||||
|
else window.open('/lms/assignments/new', '_blank')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -148,7 +148,7 @@ const getRowRoute = (row) => {
|
|||||||
return {
|
return {
|
||||||
name: 'AssignmentSubmission',
|
name: 'AssignmentSubmission',
|
||||||
params: {
|
params: {
|
||||||
assignmentName: row.assessment_name,
|
assignmentID: row.assessment_name,
|
||||||
submissionName: row.submission.name,
|
submissionName: row.submission.name,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -156,7 +156,7 @@ const getRowRoute = (row) => {
|
|||||||
return {
|
return {
|
||||||
name: 'AssignmentSubmission',
|
name: 'AssignmentSubmission',
|
||||||
params: {
|
params: {
|
||||||
assignmentName: row.assessment_name,
|
assignmentID: row.assessment_name,
|
||||||
submissionName: 'new',
|
submissionName: 'new',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="">
|
<div class="">
|
||||||
<div class="w-full flex items-center justify-between border-b pb-4">
|
<div class="w-full flex items-center justify-between pb-4">
|
||||||
<div class="font-medium text-gray-600">
|
<div class="font-medium text-gray-600">
|
||||||
{{ __('Statistics') }}
|
{{ __('Statistics') }}
|
||||||
</div>
|
</div>
|
||||||
@@ -98,9 +98,6 @@
|
|||||||
row-key="name"
|
row-key="name"
|
||||||
:options="{
|
:options="{
|
||||||
showTooltip: false,
|
showTooltip: false,
|
||||||
onRowClick: (row) => {
|
|
||||||
openStudentProgressModal(row)
|
|
||||||
},
|
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<ListHeader
|
<ListHeader
|
||||||
@@ -121,7 +118,12 @@
|
|||||||
</ListHeaderItem>
|
</ListHeaderItem>
|
||||||
</ListHeader>
|
</ListHeader>
|
||||||
<ListRows>
|
<ListRows>
|
||||||
<ListRow :row="row" v-for="row in students.data">
|
<ListRow
|
||||||
|
:row="row"
|
||||||
|
v-for="row in students.data"
|
||||||
|
class="group cursor-pointer"
|
||||||
|
@click="openStudentProgressModal(row)"
|
||||||
|
>
|
||||||
<template #default="{ column, item }">
|
<template #default="{ column, item }">
|
||||||
<ListRowItem :item="row[column.key]" :align="column.align">
|
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
@@ -140,6 +142,16 @@
|
|||||||
>
|
>
|
||||||
<ProgressBar :progress="row[column.key]" size="sm" />
|
<ProgressBar :progress="row[column.key]" size="sm" />
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="column.key == 'copy'"
|
||||||
|
class="invisible group-hover:visible"
|
||||||
|
>
|
||||||
|
<Button variant="ghost" @click="copyEmail(row)">
|
||||||
|
<template #icon>
|
||||||
|
<Clipboard class="h-4 w-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
{{ row[column.key] }}
|
{{ row[column.key] }}
|
||||||
</div>
|
</div>
|
||||||
@@ -190,7 +202,14 @@ import {
|
|||||||
ListView,
|
ListView,
|
||||||
ListRowItem,
|
ListRowItem,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { BookOpen, Plus, ShieldCheck, Trash2, User } from 'lucide-vue-next'
|
import {
|
||||||
|
BookOpen,
|
||||||
|
Clipboard,
|
||||||
|
Plus,
|
||||||
|
ShieldCheck,
|
||||||
|
Trash2,
|
||||||
|
User,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
import { ref, watch } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import StudentModal from '@/components/Modals/StudentModal.vue'
|
import StudentModal from '@/components/Modals/StudentModal.vue'
|
||||||
import { showToast } from '@/utils'
|
import { showToast } from '@/utils'
|
||||||
@@ -247,6 +266,10 @@ const getStudentColumns = () => {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
icon: 'clock',
|
icon: 'clock',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: '',
|
||||||
|
key: 'copy',
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
@@ -331,7 +354,10 @@ const getChartData = () => {
|
|||||||
const getChartOptions = (categories) => {
|
const getChartOptions = (categories) => {
|
||||||
const courseColor = '#0289F7'
|
const courseColor = '#0289F7'
|
||||||
const assessmentColor = '#E03636'
|
const assessmentColor = '#E03636'
|
||||||
const maxY = Math.ceil(students.data?.length / 10) * 10
|
const maxY =
|
||||||
|
students.data?.length % 5
|
||||||
|
? students.data?.length + (5 - (students.data?.length % 5))
|
||||||
|
: students.data?.length
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chart: {
|
chart: {
|
||||||
@@ -346,7 +372,7 @@ const getChartOptions = (categories) => {
|
|||||||
distributed: true,
|
distributed: true,
|
||||||
borderRadius: 0,
|
borderRadius: 0,
|
||||||
horizontal: false,
|
horizontal: false,
|
||||||
columnWidth: '30%',
|
columnWidth: '5%',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
colors: Object.values(categories).map((item) =>
|
colors: Object.values(categories).map((item) =>
|
||||||
@@ -362,20 +388,22 @@ const getChartOptions = (categories) => {
|
|||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
},
|
},
|
||||||
rotate: 0,
|
rotate: 0,
|
||||||
formatter: function (value) {
|
|
||||||
return value.length > 22 ? `${value.substring(0, 22)}...` : value // Trim long labels
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
yaxis: {
|
yaxis: {
|
||||||
max: maxY,
|
max: maxY,
|
||||||
min: 0,
|
min: 0,
|
||||||
stepSize: 10,
|
stepSize: 10,
|
||||||
tickAmount: maxY / 10,
|
tickAmount: maxY / 5,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyEmail = (row) => {
|
||||||
|
navigator.clipboard.writeText(row.email)
|
||||||
|
showToast(__('Success'), __('Email copied to clipboard'), 'check')
|
||||||
|
}
|
||||||
|
|
||||||
watch(students, () => {
|
watch(students, () => {
|
||||||
if (students.data?.length) {
|
if (students.data?.length) {
|
||||||
assessmentCount.value = Object.keys(students.data?.[0].assessments).length
|
assessmentCount.value = Object.keys(students.data?.[0].assessments).length
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="flex items-center text-sm font-medium space-x-2">
|
||||||
|
<span>
|
||||||
|
{{ __('What does include in preview mean?') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-xs text-gray-600 mb-1 leading-5">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'If Include in Preview is enabled for a lesson then the lesson will also be accessible to non logged in users.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div
|
<div
|
||||||
class="flex items-center text-sm font-medium space-x-2 cursor-pointer"
|
class="flex items-center text-sm font-medium space-x-2 cursor-pointer"
|
||||||
@@ -56,21 +71,6 @@
|
|||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-2">
|
|
||||||
<div class="flex items-center text-sm font-medium space-x-2">
|
|
||||||
<span>
|
|
||||||
{{ __('What does include in preview mean?') }}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="text-xs text-gray-600 mb-1 leading-5">
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'If Include in Preview is enabled for a lesson then the lesson will also be accessible to non logged in users.'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<ExplanationVideos v-model="showExplanation" :title="title" :type="type" />
|
<ExplanationVideos v-model="showExplanation" :title="title" :type="type" />
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog v-model="show" :options="{}">
|
<Dialog v-model="show" :options="{}">
|
||||||
<template #body>
|
<template #body>
|
||||||
<div class="p-5 space-y-8">
|
<div class="p-5 space-y-8 text-base">
|
||||||
<div class="flex items-center space-x-2">
|
<div class="flex items-center space-x-2">
|
||||||
<Avatar :image="student.user_image" size="3xl" />
|
<Avatar :image="student.user_image" size="3xl" />
|
||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
|
|||||||
@@ -1,58 +0,0 @@
|
|||||||
<template>
|
|
||||||
<Dialog
|
|
||||||
v-model="show"
|
|
||||||
:options="{
|
|
||||||
size: 'xl',
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
<template #body>
|
|
||||||
<div class="p-5 space-y-4">
|
|
||||||
<div class="text-lg font-semibold">
|
|
||||||
{{ __('Add a quiz to your lesson') }}
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Link
|
|
||||||
v-model="quiz"
|
|
||||||
doctype="LMS Quiz"
|
|
||||||
:label="__('Select a quiz')"
|
|
||||||
:onCreate="(value, close) => redirectToQuizForm()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="flex justify-end space-x-2">
|
|
||||||
<Button variant="solid" @click="addQuiz()">
|
|
||||||
{{ __('Save') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</Dialog>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { Dialog, Button } from 'frappe-ui'
|
|
||||||
import { onMounted, ref, nextTick } from 'vue'
|
|
||||||
import Link from '@/components/Controls/Link.vue'
|
|
||||||
|
|
||||||
const show = ref(false)
|
|
||||||
const quiz = ref(null)
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
onQuizAddition: {
|
|
||||||
type: Function,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await nextTick()
|
|
||||||
show.value = true
|
|
||||||
})
|
|
||||||
|
|
||||||
const addQuiz = () => {
|
|
||||||
props.onQuizAddition(quiz.value)
|
|
||||||
show.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const redirectToQuizForm = () => {
|
|
||||||
window.open('/lms/quizzes/new', '_blank')
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
183
frontend/src/pages/AssignmentForm.vue
Normal file
183
frontend/src/pages/AssignmentForm.vue
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
|
||||||
|
>
|
||||||
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
|
<div class="space-x-2">
|
||||||
|
<router-link
|
||||||
|
v-if="assignment.doc?.name"
|
||||||
|
:to="{
|
||||||
|
name: 'AssignmentSubmissionList',
|
||||||
|
params: {
|
||||||
|
assignmentID: assignment.doc.name,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Button>
|
||||||
|
{{ __('Submission List') }}
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
<Button variant="solid" @click="saveAssignment()">
|
||||||
|
{{ __('Save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
<div class="w-3/4 mx-auto py-5">
|
||||||
|
<div class="font-semibold mb-4">
|
||||||
|
{{ __('Details') }}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-5 mt-4 mb-8">
|
||||||
|
<FormControl
|
||||||
|
v-model="model.title"
|
||||||
|
:label="__('Title')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="model.type"
|
||||||
|
type="select"
|
||||||
|
:options="assignmentOptions"
|
||||||
|
:label="__('Type')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div class="text-xs text-gray-600 mb-2">
|
||||||
|
{{ __('Question') }}
|
||||||
|
<span class="text-ink-red-3">*</span>
|
||||||
|
</div>
|
||||||
|
<TextEditor
|
||||||
|
:content="model.question"
|
||||||
|
@change="(val) => (model.question = 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>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
Breadcrumbs,
|
||||||
|
Button,
|
||||||
|
createDocumentResource,
|
||||||
|
createResource,
|
||||||
|
FormControl,
|
||||||
|
TextEditor,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { computed, inject, onMounted, onBeforeUnmount, reactive } from 'vue'
|
||||||
|
import { showToast } from '@/utils'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
|
const user = inject('$user')
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
assignmentID: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const model = reactive({
|
||||||
|
title: '',
|
||||||
|
type: 'PDF',
|
||||||
|
question: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (
|
||||||
|
props.assignmentID == 'new' &&
|
||||||
|
!user.data?.is_moderator &&
|
||||||
|
!user.data?.is_instructor
|
||||||
|
) {
|
||||||
|
router.push({ name: 'Courses' })
|
||||||
|
}
|
||||||
|
if (props.assignmentID !== 'new') {
|
||||||
|
assignment.reload()
|
||||||
|
}
|
||||||
|
window.addEventListener('keydown', keyboardShortcut)
|
||||||
|
})
|
||||||
|
|
||||||
|
const keyboardShortcut = (e) => {
|
||||||
|
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||||
|
saveAssignment()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('keydown', keyboardShortcut)
|
||||||
|
})
|
||||||
|
|
||||||
|
const assignment = createDocumentResource({
|
||||||
|
doctype: 'LMS Assignment',
|
||||||
|
name: props.assignmentID,
|
||||||
|
auto: false,
|
||||||
|
onSuccess(data) {
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
model[key] = data[key]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const newAssignment = createResource({
|
||||||
|
url: 'frappe.client.insert',
|
||||||
|
makeParams(values) {
|
||||||
|
return {
|
||||||
|
doc: {
|
||||||
|
doctype: 'LMS Assignment',
|
||||||
|
...values,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSuccess(data) {
|
||||||
|
router.push({ name: 'AssignmentForm', params: { assignmentID: data.name } })
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveAssignment = () => {
|
||||||
|
if (props.assignmentID == 'new') {
|
||||||
|
newAssignment.submit({
|
||||||
|
...model,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
assignment.setValue.submit(
|
||||||
|
{
|
||||||
|
...model,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess(data) {
|
||||||
|
showToast(__('Success'), __('Assignment saved successfully'), 'check')
|
||||||
|
assignment.reload()
|
||||||
|
},
|
||||||
|
onError(err) {
|
||||||
|
showToast(__('Error'), __(err.messages?.[0] || err), 'x')
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => [
|
||||||
|
{
|
||||||
|
label: __('Assignments'),
|
||||||
|
route: { name: 'Assignments' },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: assignment.doc ? assignment.doc.title : __('New Assignment'),
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const assignmentOptions = computed(() => {
|
||||||
|
return [
|
||||||
|
{ label: 'PDF', value: 'PDF' },
|
||||||
|
{ label: 'Image', value: 'Image' },
|
||||||
|
{ label: 'Document', value: 'Document' },
|
||||||
|
{ label: 'Text', value: 'Text' },
|
||||||
|
{ label: 'URL', value: 'URL' },
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<div class="container py-5">
|
<div class="container py-5">
|
||||||
<div
|
<div
|
||||||
v-if="submissionResource.data"
|
v-if="submissionResource.data"
|
||||||
class="bg-blue-100 p-2 rounded-md leading-5 text-sm italic"
|
class="bg-blue-100 p-2 rounded-md leading-5 text-sm mb-4"
|
||||||
>
|
>
|
||||||
{{ __("You've successfully submitted the assignment.") }}
|
{{ __("You've successfully submitted the assignment.") }}
|
||||||
{{
|
{{
|
||||||
@@ -133,7 +133,7 @@ const answer = ref(null)
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
assignmentName: {
|
assignmentID: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
@@ -147,7 +147,7 @@ const assignment = createResource({
|
|||||||
url: 'frappe.client.get',
|
url: 'frappe.client.get',
|
||||||
params: {
|
params: {
|
||||||
doctype: 'LMS Assignment',
|
doctype: 'LMS Assignment',
|
||||||
name: props.assignmentName,
|
name: props.assignmentID,
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
@@ -191,7 +191,7 @@ const newSubmission = createResource({
|
|||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
let doc = {
|
let doc = {
|
||||||
doctype: 'LMS Assignment Submission',
|
doctype: 'LMS Assignment Submission',
|
||||||
assignment: props.assignmentName,
|
assignment: props.assignmentID,
|
||||||
member: user.data?.name,
|
member: user.data?.name,
|
||||||
}
|
}
|
||||||
if (showUploader()) {
|
if (showUploader()) {
|
||||||
@@ -256,7 +256,7 @@ const addNewSubmission = () => {
|
|||||||
router.push({
|
router.push({
|
||||||
name: 'AssignmentSubmission',
|
name: 'AssignmentSubmission',
|
||||||
params: {
|
params: {
|
||||||
assignmentName: props.assignmentName,
|
assignmentID: props.assignmentID,
|
||||||
submissionName: data.name,
|
submissionName: data.name,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@@ -278,7 +278,7 @@ const breadcrumbs = computed(() => {
|
|||||||
route: {
|
route: {
|
||||||
name: 'AssignmentSubmission',
|
name: 'AssignmentSubmission',
|
||||||
params: {
|
params: {
|
||||||
assignmentName: assignment.data?.name,
|
assignmentID: assignment.data?.name,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
202
frontend/src/pages/AssignmentSubmissionList.vue
Normal file
202
frontend/src/pages/AssignmentSubmissionList.vue
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
|
||||||
|
>
|
||||||
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
v-if="submissions.loading || submissions.data?.length"
|
||||||
|
class="md:w-3/4 md:mx-auto py-5 mx-5"
|
||||||
|
>
|
||||||
|
<div class="grid grid-cols-4 gap-5 mb-5">
|
||||||
|
<Link
|
||||||
|
doctype="LMS Assignment"
|
||||||
|
v-model="assignmentID"
|
||||||
|
:placeholder="__('Assignment')"
|
||||||
|
/>
|
||||||
|
<Link doctype="User" v-model="member" :placeholder="__('Member')" />
|
||||||
|
</div>
|
||||||
|
<ListView
|
||||||
|
:columns="submissionColumns"
|
||||||
|
:rows="submissions.data"
|
||||||
|
rowKey="name"
|
||||||
|
>
|
||||||
|
<ListHeader
|
||||||
|
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
|
||||||
|
>
|
||||||
|
<ListHeaderItem :item="item" v-for="item in submissionColumns" />
|
||||||
|
</ListHeader>
|
||||||
|
<ListRows>
|
||||||
|
<router-link
|
||||||
|
v-for="row in submissions.data"
|
||||||
|
:to="{
|
||||||
|
name: 'AssignmentSubmission',
|
||||||
|
params: {
|
||||||
|
assignmentID: row.assignment,
|
||||||
|
submissionName: row.name,
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ListRow :row="row">
|
||||||
|
<template #default="{ column, item }">
|
||||||
|
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||||
|
<div v-if="column.key == 'status'">
|
||||||
|
<Badge :theme="getStatusTheme(row[column.key])">
|
||||||
|
{{ row[column.key] }}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
{{ row[column.key] }}
|
||||||
|
</div>
|
||||||
|
</ListRowItem>
|
||||||
|
</template>
|
||||||
|
</ListRow>
|
||||||
|
</router-link>
|
||||||
|
</ListRows>
|
||||||
|
</ListView>
|
||||||
|
</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-8 mx-auto stroke-1 text-gray-500" />
|
||||||
|
<div class="text-xl font-medium">
|
||||||
|
{{ __('No submissions') }}
|
||||||
|
</div>
|
||||||
|
<!-- <div class="leading-5">
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'There are no submissions for the assignment {0}.',
|
||||||
|
).format(assignmentTitle.data?.title)
|
||||||
|
}}
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
Badge,
|
||||||
|
Breadcrumbs,
|
||||||
|
createListResource,
|
||||||
|
createResource,
|
||||||
|
ListView,
|
||||||
|
ListHeader,
|
||||||
|
ListHeaderItem,
|
||||||
|
ListRows,
|
||||||
|
ListRow,
|
||||||
|
ListRowItem,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { Pencil } from 'lucide-vue-next'
|
||||||
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
|
const user = inject('$user')
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
const router = useRouter()
|
||||||
|
const assignmentID = ref('')
|
||||||
|
const member = ref('')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!user.data?.is_instructor && !user.data?.is_moderator) {
|
||||||
|
router.push({ name: 'Courses' })
|
||||||
|
}
|
||||||
|
|
||||||
|
assignmentID.value = router.currentRoute.value.params.assignmentID
|
||||||
|
submissions.reload()
|
||||||
|
})
|
||||||
|
|
||||||
|
const getAssignmentFilters = () => {
|
||||||
|
let filters = {}
|
||||||
|
if (assignmentID.value) {
|
||||||
|
console.log(assignmentID.value)
|
||||||
|
filters.assignment = assignmentID.value
|
||||||
|
}
|
||||||
|
if (member.value) {
|
||||||
|
console.log(member.value)
|
||||||
|
filters.member = member.value
|
||||||
|
}
|
||||||
|
console.log(filters)
|
||||||
|
return filters
|
||||||
|
}
|
||||||
|
|
||||||
|
const submissions = createListResource({
|
||||||
|
doctype: 'LMS Assignment Submission',
|
||||||
|
filters: getAssignmentFilters(),
|
||||||
|
fields: [
|
||||||
|
'name',
|
||||||
|
'assignment',
|
||||||
|
'assignment_title',
|
||||||
|
'member_name',
|
||||||
|
'creation',
|
||||||
|
'status',
|
||||||
|
],
|
||||||
|
orderBy: 'creation desc',
|
||||||
|
transform(data) {
|
||||||
|
return data.map((row) => {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
creation: dayjs(row.creation).fromNow(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([assignmentID, member], () => {
|
||||||
|
console.log('watch called')
|
||||||
|
submissions.reload()
|
||||||
|
})
|
||||||
|
|
||||||
|
/* const assignmentTitle = createResource({
|
||||||
|
url: "frappe.client.get_value",
|
||||||
|
params: {
|
||||||
|
doctype: "LMS Assignment",
|
||||||
|
fieldname: "title",
|
||||||
|
filters: { name: props.assignmentID },
|
||||||
|
},
|
||||||
|
}) */
|
||||||
|
|
||||||
|
const submissionColumns = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Member',
|
||||||
|
key: 'member_name',
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Assignment',
|
||||||
|
key: 'assignment_title',
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Submitted',
|
||||||
|
key: 'creation',
|
||||||
|
width: 1,
|
||||||
|
align: 'left',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Status',
|
||||||
|
key: 'status',
|
||||||
|
width: 1,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const getStatusTheme = (status) => {
|
||||||
|
if (status === 'Pass') {
|
||||||
|
return 'green'
|
||||||
|
} else if (status === 'Not Graded') {
|
||||||
|
return 'orange'
|
||||||
|
} else {
|
||||||
|
return 'red'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: 'Assignment Submissions',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
123
frontend/src/pages/Assignments.vue
Normal file
123
frontend/src/pages/Assignments.vue
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<template>
|
||||||
|
<header
|
||||||
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
|
||||||
|
>
|
||||||
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'AssignmentForm',
|
||||||
|
params: {
|
||||||
|
assignmentID: 'new',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<Button variant="solid">
|
||||||
|
<template #prefix>
|
||||||
|
<Plus class="w-4 h-4" />
|
||||||
|
</template>
|
||||||
|
{{ __('New') }}
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div v-if="assignments.data?.length" class="md:w-3/4 md:mx-auto py-5 mx-5">
|
||||||
|
<ListView
|
||||||
|
:columns="assignmentColumns"
|
||||||
|
:rows="assignments.data"
|
||||||
|
row-key="name"
|
||||||
|
:options="{
|
||||||
|
showTooltip: false,
|
||||||
|
selectable: false,
|
||||||
|
getRowRoute: (row) => ({
|
||||||
|
name: 'AssignmentForm',
|
||||||
|
params: {
|
||||||
|
assignmentID: row.name,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
</ListView>
|
||||||
|
<div class="flex justify-center my-5">
|
||||||
|
<Button v-if="assignments.hasNextPage" @click="assignments.next()">
|
||||||
|
{{ __('Load More') }}
|
||||||
|
</Button>
|
||||||
|
</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>
|
||||||
|
<script setup>
|
||||||
|
import { Breadcrumbs, Button, createListResource, ListView } from 'frappe-ui'
|
||||||
|
import { computed, inject } from 'vue'
|
||||||
|
import { Plus, Pencil } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const user = inject('$user')
|
||||||
|
const dayjs = inject('$dayjs')
|
||||||
|
|
||||||
|
const assignmentFilter = computed(() => {
|
||||||
|
if (user.data?.is_moderator) return {}
|
||||||
|
return {
|
||||||
|
owner: user.data?.name,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const assignments = createListResource({
|
||||||
|
doctype: 'LMS Assignment',
|
||||||
|
fields: ['name', 'title', 'type', 'creation'],
|
||||||
|
filters: assignmentFilter,
|
||||||
|
orderBy: 'modified desc',
|
||||||
|
auto: true,
|
||||||
|
cache: ['assignments'],
|
||||||
|
transform(data) {
|
||||||
|
return data.map((row) => {
|
||||||
|
return {
|
||||||
|
...row,
|
||||||
|
creation: dayjs(row.creation).fromNow(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const assignmentColumns = computed(() => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: __('Title'),
|
||||||
|
key: 'title',
|
||||||
|
width: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Type'),
|
||||||
|
key: 'type',
|
||||||
|
width: 1,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Created'),
|
||||||
|
key: 'creation',
|
||||||
|
width: 1,
|
||||||
|
align: 'center',
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
const breadcrumbs = computed(() => [
|
||||||
|
{
|
||||||
|
label: 'Assignments',
|
||||||
|
route: { name: 'Assignments' },
|
||||||
|
},
|
||||||
|
])
|
||||||
|
</script>
|
||||||
@@ -42,8 +42,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div v-if="jobsList?.length">
|
<div v-if="jobsList?.length">
|
||||||
<div class="divide-y lg:w-3/4 mx-auto p-5">
|
<div class="lg:w-3/4 mx-auto p-5">
|
||||||
<div v-for="job in jobsList">
|
<div class="text-xl font-semibold mb-5">
|
||||||
|
{{ __('Find the perfect job for you') }}
|
||||||
|
</div>
|
||||||
|
<div v-for="job in jobsList" class="divide-y">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'JobDetail',
|
name: 'JobDetail',
|
||||||
|
|||||||
@@ -256,11 +256,7 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const keyboardShortcut = (e) => {
|
const keyboardShortcut = (e) => {
|
||||||
if (
|
if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
|
||||||
e.key === 's' &&
|
|
||||||
(e.ctrlKey || e.metaKey) &&
|
|
||||||
!e.target.classList.contains('ProseMirror')
|
|
||||||
) {
|
|
||||||
submitQuiz()
|
submitQuiz()
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -131,12 +131,6 @@ const routes = [
|
|||||||
component: () => import('@/pages/JobCreation.vue'),
|
component: () => import('@/pages/JobCreation.vue'),
|
||||||
props: true,
|
props: true,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/assignment-submission/:assignmentName/:submissionName',
|
|
||||||
name: 'AssignmentSubmission',
|
|
||||||
component: () => import('@/pages/AssignmentSubmission.vue'),
|
|
||||||
props: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/certified-participants',
|
path: '/certified-participants',
|
||||||
name: 'CertifiedParticipants',
|
name: 'CertifiedParticipants',
|
||||||
@@ -193,6 +187,28 @@ const routes = [
|
|||||||
name: 'Programs',
|
name: 'Programs',
|
||||||
component: () => import('@/pages/Programs.vue'),
|
component: () => import('@/pages/Programs.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/assignments',
|
||||||
|
name: 'Assignments',
|
||||||
|
component: () => import('@/pages/Assignments.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/assignments/:assignmentID',
|
||||||
|
name: 'AssignmentForm',
|
||||||
|
component: () => import('@/pages/AssignmentForm.vue'),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/assignment-submission/:assignmentID/:submissionName',
|
||||||
|
name: 'AssignmentSubmission',
|
||||||
|
component: () => import('@/pages/AssignmentSubmission.vue'),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/assignment-submissions',
|
||||||
|
name: 'AssignmentSubmissionList',
|
||||||
|
component: () => import('@/pages/AssignmentSubmissionList.vue'),
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let router = createRouter({
|
let router = createRouter({
|
||||||
|
|||||||
80
frontend/src/utils/assignment.js
Normal file
80
frontend/src/utils/assignment.js
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
import { Pencil } from 'lucide-vue-next'
|
||||||
|
import { createApp, h } from 'vue'
|
||||||
|
import AssessmentPlugin from '@/components/AssessmentPlugin.vue'
|
||||||
|
import translationPlugin from '../translation'
|
||||||
|
import { usersStore } from '../stores/user'
|
||||||
|
|
||||||
|
export class Assignment {
|
||||||
|
constructor({ data, api, readOnly }) {
|
||||||
|
this.data = data
|
||||||
|
this.readOnly = readOnly
|
||||||
|
}
|
||||||
|
|
||||||
|
static get toolbox() {
|
||||||
|
const app = createApp({
|
||||||
|
render: () =>
|
||||||
|
h(Pencil, { size: 18, strokeWidth: 1.5, color: 'black' }),
|
||||||
|
})
|
||||||
|
|
||||||
|
const div = document.createElement('div')
|
||||||
|
app.mount(div)
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: __('Assignment'),
|
||||||
|
icon: div.innerHTML,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get isReadOnlySupported() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
this.wrapper = document.createElement('div')
|
||||||
|
if (Object.keys(this.data).length) {
|
||||||
|
this.renderAssignment(this.data.assignment)
|
||||||
|
} else {
|
||||||
|
this.renderAssignmentModal()
|
||||||
|
}
|
||||||
|
return this.wrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAssignment(assignment) {
|
||||||
|
if (this.readOnly) {
|
||||||
|
const app = createApp(AssignmentBlock, {
|
||||||
|
assignment: assignment,
|
||||||
|
})
|
||||||
|
app.use(translationPlugin)
|
||||||
|
const { userResource } = usersStore()
|
||||||
|
app.provide('$user', userResource)
|
||||||
|
app.mount(this.wrapper)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.wrapper.innerHTML = `<div class='border rounded-md p-10 text-center bg-gray-50 mb-2'>
|
||||||
|
<span class="font-medium">
|
||||||
|
Assignment: ${assignment}
|
||||||
|
</span>
|
||||||
|
</div>`
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
renderAssignmentModal() {
|
||||||
|
if (this.readOnly) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const app = createApp(AssessmentPlugin, {
|
||||||
|
onAddition: (assignment) => {
|
||||||
|
this.data.assignment = assignment
|
||||||
|
this.renderAssignment(assignment)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
app.use(translationPlugin)
|
||||||
|
app.mount(this.wrapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
save(blockContent) {
|
||||||
|
return {
|
||||||
|
assignment: this.data.assignment,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { toast } from 'frappe-ui'
|
import { toast } from 'frappe-ui'
|
||||||
import { useTimeAgo } from '@vueuse/core'
|
import { useTimeAgo } from '@vueuse/core'
|
||||||
import { Quiz } from '@/utils/quiz'
|
import { Quiz } from '@/utils/quiz'
|
||||||
|
import { Assignment } from '@/utils/Assignment'
|
||||||
import { Upload } from '@/utils/upload'
|
import { Upload } from '@/utils/upload'
|
||||||
import { Markdown } from '@/utils/markdownParser'
|
import { Markdown } from '@/utils/markdownParser'
|
||||||
import Header from '@editorjs/header'
|
import Header from '@editorjs/header'
|
||||||
@@ -155,6 +156,7 @@ export function getEditorTools() {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
quiz: Quiz,
|
quiz: Quiz,
|
||||||
|
assignment: Assignment,
|
||||||
upload: Upload,
|
upload: Upload,
|
||||||
markdown: Markdown,
|
markdown: Markdown,
|
||||||
image: SimpleImage,
|
image: SimpleImage,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import QuizBlock from '@/components/QuizBlock.vue'
|
import QuizBlock from '@/components/QuizBlock.vue'
|
||||||
import QuizPlugin from '@/components/QuizPlugin.vue'
|
import AssessmentPlugin from '@/components/AssessmentPlugin.vue'
|
||||||
import { createApp, h } from 'vue'
|
import { createApp, h } from 'vue'
|
||||||
import { usersStore } from '../stores/user'
|
import { usersStore } from '../stores/user'
|
||||||
import translationPlugin from '../translation'
|
import translationPlugin from '../translation'
|
||||||
@@ -63,8 +63,8 @@ export class Quiz {
|
|||||||
if (this.readOnly) {
|
if (this.readOnly) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const app = createApp(QuizPlugin, {
|
const app = createApp(AssessmentPlugin, {
|
||||||
onQuizAddition: (quiz) => {
|
onAddition: (quiz) => {
|
||||||
this.data.quiz = quiz
|
this.data.quiz = quiz
|
||||||
this.renderQuiz(quiz)
|
this.renderQuiz(quiz)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,10 +9,11 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"title",
|
"title",
|
||||||
"grade_assignment",
|
|
||||||
"question",
|
"question",
|
||||||
"column_break_hmwv",
|
"column_break_hmwv",
|
||||||
"type",
|
"type",
|
||||||
|
"grade_assignment",
|
||||||
|
"section_break_sjti",
|
||||||
"show_answer",
|
"show_answer",
|
||||||
"answer"
|
"answer"
|
||||||
],
|
],
|
||||||
@@ -20,7 +21,8 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "question",
|
"fieldname": "question",
|
||||||
"fieldtype": "Text Editor",
|
"fieldtype": "Text Editor",
|
||||||
"label": "Question"
|
"label": "Question",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "type",
|
"fieldname": "type",
|
||||||
@@ -28,14 +30,16 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Type",
|
"label": "Type",
|
||||||
"options": "Document\nPDF\nURL\nImage\nText"
|
"options": "Document\nPDF\nURL\nImage\nText",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "title",
|
"fieldname": "title",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 1,
|
"in_standard_filter": 1,
|
||||||
"label": "Title"
|
"label": "Title",
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_hmwv",
|
"fieldname": "column_break_hmwv",
|
||||||
@@ -60,11 +64,15 @@
|
|||||||
"fieldname": "grade_assignment",
|
"fieldname": "grade_assignment",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Grade Assignment"
|
"label": "Grade Assignment"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "section_break_sjti",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-04-05 12:01:36.601160",
|
"modified": "2024-12-24 09:36:31.464508",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Assignment",
|
"name": "LMS Assignment",
|
||||||
|
|||||||
@@ -14,19 +14,17 @@
|
|||||||
"member",
|
"member",
|
||||||
"member_name",
|
"member_name",
|
||||||
"section_break_dlzh",
|
"section_break_dlzh",
|
||||||
"question",
|
|
||||||
"column_break_zvis",
|
|
||||||
"assignment_attachment",
|
"assignment_attachment",
|
||||||
"answer",
|
"answer",
|
||||||
"section_break_rqal",
|
"column_break_oqqy",
|
||||||
"status",
|
|
||||||
"evaluator",
|
"evaluator",
|
||||||
"column_break_esgd",
|
"status",
|
||||||
"comments",
|
"comments",
|
||||||
"section_break_cwaw",
|
"section_break_rqal",
|
||||||
"lesson",
|
"question",
|
||||||
|
"column_break_esgd",
|
||||||
"course",
|
"course",
|
||||||
"column_break_ygdu"
|
"lesson"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -89,8 +87,7 @@
|
|||||||
"fieldname": "evaluator",
|
"fieldname": "evaluator",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Evaluator",
|
"label": "Evaluator",
|
||||||
"options": "User",
|
"options": "User"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:!([\"URL\", \"Text\"]).includes(doc.type);",
|
"depends_on": "eval:!([\"URL\", \"Text\"]).includes(doc.type);",
|
||||||
@@ -128,14 +125,6 @@
|
|||||||
"fieldname": "column_break_esgd",
|
"fieldname": "column_break_esgd",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "section_break_cwaw",
|
|
||||||
"fieldtype": "Section Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "column_break_ygdu",
|
|
||||||
"fieldtype": "Column Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"depends_on": "eval:([\"URL\", \"Text\"]).includes(doc.type);",
|
"depends_on": "eval:([\"URL\", \"Text\"]).includes(doc.type);",
|
||||||
"fieldname": "answer",
|
"fieldname": "answer",
|
||||||
@@ -148,14 +137,14 @@
|
|||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "column_break_zvis",
|
"fieldname": "column_break_oqqy",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2024-04-05 15:57:22.758563",
|
"modified": "2024-12-24 21:22:35.212732",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Assignment Submission",
|
"name": "LMS Assignment Submission",
|
||||||
|
|||||||
@@ -1487,11 +1487,19 @@ def get_batch_students(batch):
|
|||||||
|
|
||||||
detail.courses_completed = courses_completed
|
detail.courses_completed = courses_completed
|
||||||
detail.assessments_completed = assessments_completed
|
detail.assessments_completed = assessments_completed
|
||||||
detail.progress = (
|
|
||||||
(courses_completed + assessments_completed)
|
if len(batch_courses) or len(assessments):
|
||||||
/ (len(batch_courses) + len(assessments))
|
detail.progress = flt(
|
||||||
* 100
|
(
|
||||||
)
|
(courses_completed + assessments_completed)
|
||||||
|
/ (len(batch_courses) + len(assessments))
|
||||||
|
* 100
|
||||||
|
),
|
||||||
|
2,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
detail.progress = 0
|
||||||
|
|
||||||
students.append(detail)
|
students.append(detail)
|
||||||
|
|
||||||
return students
|
return students
|
||||||
|
|||||||
Reference in New Issue
Block a user