Merge pull request #1396 from pateljannat/assignment-in-course-issue

fix: assignment and quiz rendering issue in courses
This commit is contained in:
Jannat Patel
2025-03-24 15:10:19 +05:30
committed by GitHub
15 changed files with 114 additions and 110 deletions

View File

@@ -66,6 +66,7 @@ declare module 'vue' {
MobileLayout: typeof import('./src/components/MobileLayout.vue')['default'] MobileLayout: typeof import('./src/components/MobileLayout.vue')['default']
MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default'] MultiSelect: typeof import('./src/components/Controls/MultiSelect.vue')['default']
NoPermission: typeof import('./src/components/NoPermission.vue')['default'] NoPermission: typeof import('./src/components/NoPermission.vue')['default']
NoSidebarLayout: typeof import('./src/components/NoSidebarLayout.vue')['default']
NotPermitted: typeof import('./src/components/NotPermitted.vue')['default'] NotPermitted: typeof import('./src/components/NotPermitted.vue')['default']
OnboardingBanner: typeof import('./src/components/OnboardingBanner.vue')['default'] OnboardingBanner: typeof import('./src/components/OnboardingBanner.vue')['default']
PageModal: typeof import('./src/components/Modals/PageModal.vue')['default'] PageModal: typeof import('./src/components/Modals/PageModal.vue')['default']

View File

@@ -8,18 +8,34 @@
<script setup> <script setup>
import { Toasts } from 'frappe-ui' import { Toasts } from 'frappe-ui'
import { Dialogs } from '@/utils/dialogs' import { Dialogs } from '@/utils/dialogs'
import { computed, onMounted, onUnmounted } from 'vue' import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useScreenSize } from './utils/composables' import { useScreenSize } from './utils/composables'
import DesktopLayout from './components/DesktopLayout.vue' import DesktopLayout from './components/DesktopLayout.vue'
import MobileLayout from './components/MobileLayout.vue' import MobileLayout from './components/MobileLayout.vue'
import NoSidebarLayout from './components/NoSidebarLayout.vue'
import { stopSession } from '@/telemetry' import { stopSession } from '@/telemetry'
import { init as initTelemetry } from '@/telemetry' import { init as initTelemetry } from '@/telemetry'
import { usersStore } from '@/stores/user' import { usersStore } from '@/stores/user'
import { useRouter } from 'vue-router'
const screenSize = useScreenSize() const screenSize = useScreenSize()
let { userResource } = usersStore() let { userResource } = usersStore()
const router = useRouter()
const noSidebar = ref(false)
router.beforeEach((to, from, next) => {
if (to.query.fromLesson) {
noSidebar.value = true
} else {
noSidebar.value = false
}
next()
})
const Layout = computed(() => { const Layout = computed(() => {
if (noSidebar.value) {
return NoSidebarLayout
}
if (screenSize.width < 640) { if (screenSize.width < 640) {
return MobileLayout return MobileLayout
} else { } else {
@@ -28,11 +44,11 @@ const Layout = computed(() => {
}) })
onMounted(async () => { onMounted(async () => {
if (!userResource.data) return if (userResource.data) await initTelemetry()
await initTelemetry()
}) })
onUnmounted(() => { onUnmounted(() => {
noSidebar.value = false
stopSession() stopSession()
}) })
</script> </script>

View File

@@ -188,7 +188,12 @@ const addQuizzes = () => {
label: 'Quizzes', label: 'Quizzes',
icon: 'CircleHelp', icon: 'CircleHelp',
to: 'Quizzes', to: 'Quizzes',
activeFor: ['Quizzes', 'QuizForm'], activeFor: [
'Quizzes',
'QuizForm',
'QuizSubmissionList',
'QuizSubmission',
],
}) })
} }
} }
@@ -199,7 +204,12 @@ const addAssignments = () => {
label: 'Assignments', label: 'Assignments',
icon: 'Pencil', icon: 'Pencil',
to: 'Assignments', to: 'Assignments',
activeFor: ['Assignments', 'AssignmentForm'], activeFor: [
'Assignments',
'AssignmentForm',
'AssignmentSubmissionList',
'AssignmentSubmission',
],
}) })
} }
} }

View File

@@ -1,10 +1,13 @@
<template> <template>
<div <div
v-if="assignment.data" v-if="assignment.data"
class="grid grid-cols-[60%,40%] h-full" class="grid grid-cols-2 h-full"
:class="{ 'border rounded-lg': !showTitle }" :class="{ 'border rounded-lg overflow-auto': !showTitle }"
> >
<div class="border-r p-5 overflow-y-auto h-[calc(100vh-3.2rem)]"> <div
class="border-r p-5 overflow-y-auto h-[calc(100vh-3.2rem)]"
:class="{ 'h-full': !showTitle }"
>
<div v-if="showTitle" class="text-lg font-semibold mb-5 text-ink-gray-9"> <div v-if="showTitle" class="text-lg font-semibold mb-5 text-ink-gray-9">
<div v-if="submissionName === 'new'"> <div v-if="submissionName === 'new'">
{{ __('Submission by') }} {{ user.data?.full_name }} {{ __('Submission by') }} {{ user.data?.full_name }}
@@ -50,7 +53,7 @@
!['Pass', 'Fail'].includes(submissionResource.doc?.status) && !['Pass', 'Fail'].includes(submissionResource.doc?.status) &&
submissionResource.doc?.owner == user.data?.name submissionResource.doc?.owner == user.data?.name
" "
class="bg-surface-blue-2 p-3 rounded-md leading-5 text-sm mb-4" class="bg-surface-blue-2 text-ink-blue-2 p-3 rounded-md leading-5 text-sm mb-4"
> >
{{ __("You've successfully submitted the assignment.") }} {{ __("You've successfully submitted the assignment.") }}
{{ {{
@@ -115,14 +118,8 @@
:readonly="!canModifyAssignment" :readonly="!canModifyAssignment"
/> />
</div> </div>
<div v-if="true"> <div v-else>
<div class="text-sm mb-4"> <div class="text-sm mb-2 text-ink-gray-7">
{{ __('Write your answer here') }}
</div>
<FormControl />
</div>
<!-- <div v-else>
<div class="text-sm mb-4">
{{ __('Write your answer here') }} {{ __('Write your answer here') }}
</div> </div>
<TextEditor <TextEditor
@@ -132,7 +129,7 @@
:fixedMenu="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]" editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
/> />
</div> --> </div>
<div <div
v-if=" v-if="
@@ -144,9 +141,10 @@
<div class="text-sm text-ink-gray-5 font-medium mb-2"> <div class="text-sm text-ink-gray-5 font-medium mb-2">
{{ __('Comments by Evaluator') }}: {{ __('Comments by Evaluator') }}:
</div> </div>
<div class="leading-5"> <div
{{ submissionResource.doc.comments }} class="leading-5 text-ink-gray-9"
</div> v-html="submissionResource.doc.comments"
></div>
</div> </div>
<!-- Grading --> <!-- Grading -->
@@ -204,7 +202,6 @@ const answer = ref(null)
const comments = ref(null) const comments = ref(null)
const router = useRouter() const router = useRouter()
const user = inject('$user') const user = inject('$user')
const showTitle = router.currentRoute.value.name == 'AssignmentSubmission'
const isDirty = ref(false) const isDirty = ref(false)
const props = defineProps({ const props = defineProps({
@@ -216,6 +213,10 @@ const props = defineProps({
type: String, type: String,
default: 'new', default: 'new',
}, },
showTitle: {
type: Boolean,
default: true,
},
}) })
onMounted(() => { onMounted(() => {
@@ -359,6 +360,7 @@ const addNewSubmission = () => {
assignmentID: props.assignmentID, assignmentID: props.assignmentID,
submissionName: data.name, submissionName: data.name,
}, },
query: { fromLesson: router.currentRoute.value.query.fromLesson },
}) })
} else { } else {
markLessonProgress() markLessonProgress()

View File

@@ -1,46 +0,0 @@
<template>
<Assignment
v-if="user.data && submission.data"
:assignmentID="assignmentID"
:submissionName="submission.data?.name || 'new'"
/>
<div v-else class="border rounded-md text-center py-20">
<div>
{{ __('Please login to access the assignment.') }}
</div>
<Button @click="redirectToLogin()" class="mt-2">
<span>
{{ __('Login') }}
</span>
</Button>
</div>
</template>
<script setup>
import { inject } from 'vue'
import { Button, createResource } from 'frappe-ui'
import Assignment from '@/components/Assignment.vue'
const user = inject('$user')
const props = defineProps({
assignmentID: {
type: String,
required: true,
},
})
const submission = createResource({
url: 'frappe.client.get_value',
makeParams(values) {
return {
doctype: 'LMS Assignment Submission',
fieldname: 'name',
filters: {
assignment: props.assignmentID,
member: user.data?.name,
},
}
},
auto: true,
})
</script>

View File

@@ -72,7 +72,7 @@
<div <div
class="outline-lesson pl-8 py-2 pr-4 text-ink-gray-9" class="outline-lesson pl-8 py-2 pr-4 text-ink-gray-9"
:class=" :class="
isActiveLesson(lesson.number) ? 'bg-surface-selected' : '' isActiveLesson(lesson.number) ? 'bg-surface-gray-3' : ''
" "
> >
<router-link <router-link

View File

@@ -0,0 +1,11 @@
<template>
<div class="relative flex h-full flex-col">
<div class="h-full flex-1">
<div class="flex h-screen text-base bg-surface-white">
<div class="w-full overflow-auto" id="scrollContainer">
<slot />
</div>
</div>
</div>
</div>
</template>

View File

@@ -1,7 +1,7 @@
<template> <template>
<div v-if="quiz.data"> <div v-if="quiz.data">
<div <div
class="bg-surface-blue-2 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-ink-blue-800" class="bg-surface-blue-2 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-ink-blue-2"
> >
<div class="leading-5"> <div class="leading-5">
{{ {{
@@ -29,7 +29,7 @@
).format(quiz.data.passing_percentage) ).format(quiz.data.passing_percentage)
}} }}
</div> </div>
<div v-if="quiz.data.max_attempts" class="leading-relaxed"> <div v-if="quiz.data.max_attempts" class="leading-5">
{{ {{
__('You can attempt this quiz {0}.').format( __('You can attempt this quiz {0}.').format(
quiz.data.max_attempts == 1 quiz.data.max_attempts == 1
@@ -52,7 +52,7 @@
<div v-if="activeQuestion == 0"> <div v-if="activeQuestion == 0">
<div class="border text-center p-20 rounded-md"> <div class="border text-center p-20 rounded-md">
<div class="font-semibold text-lg"> <div class="font-semibold text-lg text-ink-gray-9">
{{ quiz.data.title }} {{ quiz.data.title }}
</div> </div>
<Button <Button
@@ -67,7 +67,7 @@
{{ __('Start') }} {{ __('Start') }}
</span> </span>
</Button> </Button>
<div v-else> <div v-else class="leading-5 text-ink-gray-7">
{{ {{
__( __(
'You have already exceeded the maximum number of attempts allowed for this quiz.' 'You have already exceeded the maximum number of attempts allowed for this quiz.'
@@ -222,11 +222,14 @@
</div> </div>
</div> </div>
</div> </div>
<div v-else class="border rounded-md p-20 text-center space-y-4"> <div v-else class="border rounded-md p-20 text-center space-y-2">
<div class="text-lg font-semibold"> <div class="text-lg font-semibold text-ink-gray-9">
{{ __('Quiz Summary') }} {{ __('Quiz Summary') }}
</div> </div>
<div v-if="quizSubmission.data.is_open_ended"> <div
v-if="quizSubmission.data.is_open_ended"
class="leading-5 text-ink-gray-7"
>
{{ {{
__( __(
"Your submission has been successfully saved. The instructor will review and grade it shortly, and you'll be notified of your final result." "Your submission has been successfully saved. The instructor will review and grade it shortly, and you'll be notified of your final result."
@@ -613,7 +616,6 @@ const getInstructions = (question) => {
} }
const markLessonProgress = () => { const markLessonProgress = () => {
console.log(router)
if (router.currentRoute.value.name == 'Lesson') { if (router.currentRoute.value.name == 'Lesson') {
call('lms.lms.api.mark_lesson_progress', { call('lms.lms.api.mark_lesson_progress', {
course: router.currentRoute.value.params.courseName, course: router.currentRoute.value.params.courseName,

View File

@@ -2,7 +2,9 @@
<button <button
v-if="link && !link.onlyMobile" v-if="link && !link.onlyMobile"
class="flex h-7 cursor-pointer items-center rounded text-ink-gray-8 duration-300 ease-in-out focus:outline-none focus:transition-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-outline-gray-3" class="flex h-7 cursor-pointer items-center rounded text-ink-gray-8 duration-300 ease-in-out focus:outline-none focus:transition-none focus-visible:rounded focus-visible:ring-2 focus-visible:ring-outline-gray-3"
:class="isActive ? 'bg-surface-white shadow-sm' : 'hover:bg-surface-gray-2'" :class="
isActive ? 'bg-surface-selected shadow-sm' : 'hover:bg-surface-gray-2'
"
@click="handleClick" @click="handleClick"
> >
<div <div

View File

@@ -1,19 +1,25 @@
<template> <template>
<header <header
v-if="!fromLesson"
class="flex justify-between sticky top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5" class="flex justify-between sticky top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs :items="breadcrumbs" /> <Breadcrumbs :items="breadcrumbs" />
</header> </header>
<div class="overflow-hidden h-[calc(100vh-3.2rem)]"> <div class="overflow-hidden h-[calc(100vh-3.2rem)]">
<Assignment :assignmentID="assignmentID" :submissionName="submissionName" /> <Assignment
:assignmentID="assignmentID"
:submissionName="submissionName"
:showTitle="!fromLesson"
/>
</div> </div>
</template> </template>
<script setup> <script setup>
import { Breadcrumbs, createResource } from 'frappe-ui' import { Breadcrumbs, createResource } from 'frappe-ui'
import { computed, inject, onMounted } from 'vue' import { computed, inject, onMounted, onBeforeUnmount, ref } from 'vue'
import Assignment from '@/components/Assignment.vue' import Assignment from '@/components/Assignment.vue'
const user = inject('$user') const user = inject('$user')
const fromLesson = ref(false)
const props = defineProps({ const props = defineProps({
assignmentID: { assignmentID: {
@@ -42,6 +48,10 @@ onMounted(() => {
if (!user.data) { if (!user.data) {
window.location.href = '/login' window.location.href = '/login'
} }
if (new URLSearchParams(window.location.search).get('fromLesson')) {
fromLesson.value = true
}
}) })
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {

View File

@@ -587,11 +587,6 @@ updateDocumentTitle(pageMeta)
line-height: 1.7; line-height: 1.7;
} }
iframe {
border-top: 3px solid theme('colors.gray.700');
border-bottom: 3px solid theme('colors.gray.700');
}
.tc-table { .tc-table {
border-left: 1px solid #e8e8eb; border-left: 1px solid #e8e8eb;
} }

View File

@@ -1,27 +1,36 @@
<template> <template>
<header <header
v-if="!fromLesson"
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5" class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs :items="breadcrumbs" /> <Breadcrumbs :items="breadcrumbs" />
</header> </header>
<div class="md:w-7/12 md:mx-auto mx-4 py-10"> <div
class="md:w-7/12 md:mx-auto mx-4 py-10"
:class="{ 'pt-4 md:w-full': fromLesson }"
>
<Quiz :quizName="quizID" /> <Quiz :quizName="quizID" />
</div> </div>
</template> </template>
<script setup> <script setup>
import Quiz from '@/components/Quiz.vue' import Quiz from '@/components/Quiz.vue'
import { createResource, Breadcrumbs } from 'frappe-ui' import { createResource, Breadcrumbs } from 'frappe-ui'
import { computed, inject, onMounted } from 'vue' import { computed, inject, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { updateDocumentTitle } from '@/utils' import { updateDocumentTitle } from '@/utils'
const user = inject('$user') const user = inject('$user')
const router = useRouter() const router = useRouter()
const fromLesson = ref(false)
onMounted(() => { onMounted(() => {
if (!user.data) { if (!user.data) {
router.push({ name: 'Courses' }) router.push({ name: 'Courses' })
} }
if (new URLSearchParams(window.location.search).get('fromLesson')) {
fromLesson.value = true
}
}) })
const props = defineProps({ const props = defineProps({

View File

@@ -1,11 +1,9 @@
import { Pencil } from 'lucide-vue-next' import { Pencil } from 'lucide-vue-next'
import { createApp, h } from 'vue' import { createApp, h } from 'vue'
import AssessmentPlugin from '@/components/AssessmentPlugin.vue' import AssessmentPlugin from '@/components/AssessmentPlugin.vue'
import AssignmentBlock from '@/components/AssignmentBlock.vue'
import translationPlugin from '../translation' import translationPlugin from '../translation'
import { usersStore } from '@/stores/user' import { usersStore } from '@/stores/user'
import router from '../router' import { call } from 'frappe-ui'
import { FrappeUI, setConfig, frappeRequest, pageMetaPlugin } from 'frappe-ui'
export class Assignment { export class Assignment {
constructor({ data, api, readOnly }) { constructor({ data, api, readOnly }) {
@@ -44,17 +42,18 @@ export class Assignment {
renderAssignment(assignment) { renderAssignment(assignment) {
if (this.readOnly) { if (this.readOnly) {
const app = createApp(AssignmentBlock, {
assignmentID: assignment,
})
app.use(FrappeUI)
setConfig('resourceFetcher', frappeRequest)
app.use(translationPlugin)
app.use(router)
app.use(pageMetaPlugin)
const { userResource } = usersStore() const { userResource } = usersStore()
app.provide('$user', userResource) call('frappe.client.get_value', {
app.mount(this.wrapper) doctype: 'LMS Assignment Submission',
filters: {
assignment: assignment,
member: userResource.data?.name,
},
fieldname: ['name'],
}).then((data) => {
let submission = data.name || 'new'
this.wrapper.innerHTML = `<iframe src="/lms/assignment-submission/${assignment}/${submission}?fromLesson=1" class="w-full h-[500px]"></iframe>`
})
return return
} }
this.wrapper.innerHTML = `<div class='border rounded-md p-10 text-center bg-surface-menu-bar mb-2'> this.wrapper.innerHTML = `<div class='border rounded-md p-10 text-center bg-surface-menu-bar mb-2'>

View File

@@ -43,14 +43,7 @@ export class Quiz {
renderQuiz(quiz) { renderQuiz(quiz) {
if (this.readOnly) { if (this.readOnly) {
const app = createApp(QuizBlock, { this.wrapper.innerHTML = `<iframe src="/lms/quiz/${quiz}?fromLesson=1" class="w-full h-[500px]"></iframe>`
quiz: quiz,
})
app.use(translationPlugin)
app.use(router)
const { userResource } = usersStore()
app.provide('$user', userResource)
app.mount(this.wrapper)
return return
} }
this.wrapper.innerHTML = `<div class='border rounded-md p-10 text-center bg-surface-menu-bar mb-2'> this.wrapper.innerHTML = `<div class='border rounded-md p-10 text-center bg-surface-menu-bar mb-2'>

View File

@@ -56,11 +56,11 @@ export class Upload {
app.mount(this.wrapper) app.mount(this.wrapper)
return return
} else if (file.file_type == 'PDF') { } else if (file.file_type == 'PDF') {
this.wrapper.innerHTML = `<iframe src="https://docs.google.com/viewer?url=${ this.wrapper.innerHTML = `<iframe src="${
window.location.origin window.location.origin
}${encodeURI( }${encodeURI(
file.file_url file.file_url
)}&embedded=true" width='100%' height='700px' class="mb-4" type="application/pdf"></iframe>` )}" width='100%' height='700px' class="mb-4" type="application/pdf"></iframe>`
return return
} else { } else {
this.wrapper.innerHTML = `<img class="mb-4" src=${encodeURI( this.wrapper.innerHTML = `<img class="mb-4" src=${encodeURI(