feat: quiz base
This commit is contained in:
@@ -16,7 +16,8 @@
|
|||||||
"tailwindcss": "^3.2.7",
|
"tailwindcss": "^3.2.7",
|
||||||
"vue": "^3.2.25",
|
"vue": "^3.2.25",
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "^1.11.6",
|
||||||
"vue-router": "^4.0.12"
|
"vue-router": "^4.0.12",
|
||||||
|
"markdown-it": "^14.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^2.0.0",
|
"@vitejs/plugin-vue": "^2.0.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="text-base">
|
<div class="course-outline text-base">
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<Disclosure v-slot="{ open }" v-for="(chapter, index) in outline.data" :key="chapter.name" :defaultOpen="index == 0">
|
<Disclosure v-slot="{ open }" v-for="(chapter, index) in outline.data" :key="chapter.name" :defaultOpen="chapter.idx == route.params.chapterNumber">
|
||||||
<DisclosureButton class="flex w-full px-2 pt-2 pb-3">
|
<DisclosureButton class="flex w-full px-2 pt-2 pb-3">
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
:class="{'rotate-90 transform duration-200' : open, 'duration-200' : !open, 'open': index == 1}"
|
:class="{'rotate-90 transform duration-200' : open, 'duration-200' : !open, 'open': index == 1}"
|
||||||
@@ -11,13 +11,24 @@
|
|||||||
{{ chapter.title }}
|
{{ chapter.title }}
|
||||||
</div>
|
</div>
|
||||||
</DisclosureButton>
|
</DisclosureButton>
|
||||||
<DisclosurePanel class="px-10 pb-4">
|
<DisclosurePanel class="pb-2">
|
||||||
<div v-for="lesson in chapter.lessons" :key="lesson.name">
|
<div v-for="lesson in chapter.lessons" :key="lesson.name">
|
||||||
<div class="flex items-center text-base mb-3">
|
<div class="outline-lesson mb-2 px-8">
|
||||||
<MonitorPlay v-if="lesson.icon === 'icon-youtube'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
|
<router-link :to='{
|
||||||
<HelpCircle v-else-if="lesson.icon === 'icon-quiz'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
|
name: "Lesson",
|
||||||
<FileText v-else-if="lesson.icon === 'icon-list'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
|
params: {
|
||||||
{{ lesson.title }}
|
courseName: courseName,
|
||||||
|
chapterNumber: lesson.number.split(".")[0],
|
||||||
|
lessonNumber: lesson.number.split(".")[1],
|
||||||
|
}
|
||||||
|
}'>
|
||||||
|
<div class="flex items-center text-base">
|
||||||
|
<MonitorPlay v-if="lesson.icon === 'icon-youtube'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
|
||||||
|
<HelpCircle v-else-if="lesson.icon === 'icon-quiz'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
|
||||||
|
<FileText v-else-if="lesson.icon === 'icon-list'" class="h-4 w-4 text-gray-900 stroke-1 mr-2"/>
|
||||||
|
{{ lesson.title }}
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DisclosurePanel>
|
</DisclosurePanel>
|
||||||
@@ -29,7 +40,9 @@
|
|||||||
import { createResource } from "frappe-ui";
|
import { createResource } from "frappe-ui";
|
||||||
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
|
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/vue';
|
||||||
import { ChevronRight, MonitorPlay, HelpCircle, FileText } from 'lucide-vue-next';
|
import { ChevronRight, MonitorPlay, HelpCircle, FileText } from 'lucide-vue-next';
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
courseName: {
|
courseName: {
|
||||||
type: String,
|
type: String,
|
||||||
@@ -45,4 +58,10 @@ const outline = createResource({
|
|||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
<style>
|
||||||
|
.outline-lesson:has(.router-link-active) {
|
||||||
|
background-color: theme('colors.gray.100');
|
||||||
|
padding: 0.5rem 0 0.5rem 2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -82,7 +82,7 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
membership: {
|
membership: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ const reviews = createResource({
|
|||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
});
|
});
|
||||||
console.log(reviews)
|
|
||||||
const rating_percent = computed(() => {
|
const rating_percent = computed(() => {
|
||||||
let rating_count = {};
|
let rating_count = {};
|
||||||
let rating_percent = {};
|
let rating_percent = {};
|
||||||
|
|||||||
83
frontend/src/components/Quiz.vue
Normal file
83
frontend/src/components/Quiz.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="quiz.doc">
|
||||||
|
<div v-if="activeQuestion == 0">
|
||||||
|
<div class="bg-blue-100 py-2 px-2 mb-4 rounded-md text-sm text-blue-800">
|
||||||
|
<div class="leading-relaxed">
|
||||||
|
{{ __("This quiz consists of {0} questions.").format(quiz.doc.questions.length) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="quiz.doc.passing_percentage" class="leading-relaxed">
|
||||||
|
{{ __("You will have to get {0}% correct answers in order to pass the quiz.").format(quiz.doc.passing_percentage) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="quiz.doc.max_attempts" class="leading-relaxed">
|
||||||
|
{{ __("You can attempt this quiz {0}.").format(quiz.doc.max_attempts == 1 ? "1 time" : `${quiz.doc.max_attempts} times`) }}
|
||||||
|
</div>
|
||||||
|
<div v-if="quiz.doc.time" class="leading-relaxed">
|
||||||
|
{{ __("The quiz has a time limit.For each question you will be given { 0} seconds.").format(quiz.doc.time) }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="border text-center p-20 font-semibold text-lg rounded-md">
|
||||||
|
<div>
|
||||||
|
{{ quiz.doc.title }}
|
||||||
|
</div>
|
||||||
|
<Button @click="startQuiz" class="mt-2">
|
||||||
|
<span>
|
||||||
|
{{ __("Start") }}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<div v-for="index in quiz.doc.questions.length">
|
||||||
|
<div v-if="index == activeQuestion">
|
||||||
|
{{ questionDetails }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup>
|
||||||
|
import { createDocumentResource, Button } from 'frappe-ui';
|
||||||
|
import { ref, watch, inject } from 'vue';
|
||||||
|
|
||||||
|
const user = inject("$user");
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
quizName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const activeQuestion = ref(0);
|
||||||
|
const currentQuestion = ref("");
|
||||||
|
|
||||||
|
const quiz = createDocumentResource({
|
||||||
|
doctype: "LMS Quiz",
|
||||||
|
name: props.quizName,
|
||||||
|
cache: ["quiz", props.quizName],
|
||||||
|
});
|
||||||
|
console.log(user.data)
|
||||||
|
if (user.data) {
|
||||||
|
quiz.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
const questionDetails = createDocumentResource({
|
||||||
|
doctype: "LMS Question",
|
||||||
|
name: currentQuestion.value,
|
||||||
|
cache: ["question", props.quizName, currentQuestion.value],
|
||||||
|
});
|
||||||
|
console.log(questionDetails)
|
||||||
|
const startQuiz = () => {
|
||||||
|
activeQuestion.value = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(activeQuestion, (value) => {
|
||||||
|
if (value > 0) {
|
||||||
|
currentQuestion.value = quiz.doc.questions[value - 1];
|
||||||
|
console.log(currentQuestion.value)
|
||||||
|
console.log(questionDetails)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -32,9 +32,5 @@ app.mount('#app')
|
|||||||
const { userResource } = usersStore()
|
const { userResource } = usersStore()
|
||||||
let { isLoggedIn } = sessionStore()
|
let { isLoggedIn } = sessionStore()
|
||||||
|
|
||||||
if (isLoggedIn) {
|
|
||||||
await userResource.reload()
|
|
||||||
}
|
|
||||||
|
|
||||||
app.provide('$user', userResource)
|
app.provide('$user', userResource)
|
||||||
app.config.globalProperties.$user = userResource
|
app.config.globalProperties.$user = userResource
|
||||||
|
|||||||
@@ -4,10 +4,43 @@
|
|||||||
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
<Breadcrumbs class="h-7" :items="breadcrumbs" />
|
||||||
</header>
|
</header>
|
||||||
<div class="grid grid-cols-[70%,30%] h-full">
|
<div class="grid grid-cols-[70%,30%] h-full">
|
||||||
<div class="border-r-2 container pt-5 pb-10">
|
<div v-if="lesson.data.no_preview" class="border-r-2 text-center pt-10">
|
||||||
<div class="text-3xl font-semibold">
|
<p class="mb-4">
|
||||||
{{ lesson.data.title }}
|
{{ __("This lesson is not available for preview. Please enroll in the course to access it.") }}
|
||||||
|
</p>
|
||||||
|
<router-link :to='{ name: "CourseDetail", params: { courseName: courseName } }'>
|
||||||
|
<Button variant="solid">
|
||||||
|
{{ __("Start Learning") }}
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div v-else class="border-r-2 container pt-5 pb-10">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="text-3xl font-semibold">
|
||||||
|
{{ lesson.data.title }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<router-link v-if="lesson.data.prev" :to='{name: "Lesson", params: {
|
||||||
|
courseName: courseName,
|
||||||
|
chapterNumber: lesson.data.prev.split(".")[0],
|
||||||
|
lessonNumber: lesson.data.prev.split(".")[1]
|
||||||
|
}}'>
|
||||||
|
<Button class="mr-2">
|
||||||
|
<ChevronLeft class="w-4 h-4 stroke-1"/>
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
<router-link v-if="lesson.data.next" :to='{name: "Lesson", params: {
|
||||||
|
courseName: courseName,
|
||||||
|
chapterNumber: lesson.data.next.split(".")[0],
|
||||||
|
lessonNumber: lesson.data.next.split(".")[1]
|
||||||
|
}}'>
|
||||||
|
<Button>
|
||||||
|
<ChevronRight class="w-4 h-4 stroke-1"/>
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center mt-2">
|
<div class="flex items-center mt-2">
|
||||||
<span class="mr-1" :class="{ 'avatar-group overlap': course.data.instructors.length > 1 }">
|
<span class="mr-1" :class="{ 'avatar-group overlap': course.data.instructors.length > 1 }">
|
||||||
<UserAvatar v-for="instructor in course.data.instructors" :user="instructor" />
|
<UserAvatar v-for="instructor in course.data.instructors" :user="instructor" />
|
||||||
@@ -22,7 +55,46 @@
|
|||||||
{{ course.data.instructors[0].first_name }} and {{ course.data.instructors.length - 1 }} others
|
{{ course.data.instructors[0].first_name }} and {{ course.data.instructors.length - 1 }} others
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-html="lesson.data.rendered_content" class="lesson-content mt-6"></div>
|
<!-- <div v-html="lesson.data.rendered_content" class="lesson-content mt-6"></div> -->
|
||||||
|
<div class="lesson-content mt-6">
|
||||||
|
<div v-for="block in lesson.data.body.split('\n\n')">
|
||||||
|
<div v-if='block.includes("{{ YouTubeVideo")'>
|
||||||
|
<iframe class="youtube-video" :src="getYouTubeVideoSource(block)" width="100%" height="400" frameborder="0" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
<div v-else-if='block.includes("{{ Quiz")'>
|
||||||
|
<Quiz v-if="user" :quizName="getId(block)"/>
|
||||||
|
<div v-else>
|
||||||
|
<div>
|
||||||
|
{{ __("Please login to access this quiz.") }}
|
||||||
|
</div>
|
||||||
|
<Button @click="window.location.href = `/login?redirect-to=/courses/${courseName}`">
|
||||||
|
<span>
|
||||||
|
{{ __("Login") }}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if='block.includes("{{ Video")'>
|
||||||
|
<video controls width='100%' controlsList='nodownload'>
|
||||||
|
<source :src="getId(block)" type='video/mp4'>
|
||||||
|
</video>
|
||||||
|
</div>
|
||||||
|
<div v-else-if='block.includes("{{ PDF")'>
|
||||||
|
<iframe :src="getPDFSource(block)" width="100%" height="400" frameborder="0" allowfullscreen></iframe>
|
||||||
|
</div>
|
||||||
|
<div v-else-if='block.includes("{{ Audio")'>
|
||||||
|
<audio width='100%' controls controlsList='nodownload'>
|
||||||
|
<source :src="getId(block)" type='audio/mp3'>
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
<div v-else-if='block.includes("{{ Embed")'>
|
||||||
|
<iframe width="100%" height="400" :src="getId(block)" frameborder="0" allowfullscreen>
|
||||||
|
</iframe>
|
||||||
|
</div>
|
||||||
|
<div v-else v-html="markdown.render(block)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sticky top-10">
|
<div class="sticky top-10">
|
||||||
<div class="bg-gray-50 p-5 border-b-2">
|
<div class="bg-gray-50 p-5 border-b-2">
|
||||||
@@ -37,22 +109,30 @@
|
|||||||
:style="{ width: Math.ceil(course.data.membership.progress) + '%' }"></div>
|
:style="{ width: Math.ceil(course.data.membership.progress) + '%' }"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<CourseOutline :courseName="lesson.data.course" />
|
<CourseOutline :courseName="courseName" :key="chapterNumber" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createResource, Breadcrumbs } from "frappe-ui";
|
import { createResource, Breadcrumbs, Button } from "frappe-ui";
|
||||||
import { computed, onMounted, onBeforeMount, onUnmounted, inject } from "vue";
|
import { computed, watch, onBeforeMount, onUnmounted, inject } from "vue";
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import CourseOutline from '@/components/CourseOutline.vue';
|
import CourseOutline from '@/components/CourseOutline.vue';
|
||||||
import UserAvatar from '@/components/UserAvatar.vue';
|
import UserAvatar from '@/components/UserAvatar.vue';
|
||||||
|
import { useRoute } from "vue-router";
|
||||||
|
import MarkdownIt from "markdown-it";
|
||||||
|
import { ChevronLeft, ChevronRight } from "lucide-vue-next";
|
||||||
|
import Quiz from '@/components/Quiz.vue';
|
||||||
|
|
||||||
const user = inject("$user");
|
const user = inject("$user");
|
||||||
|
const route = useRoute();
|
||||||
|
const markdown = new MarkdownIt({
|
||||||
|
html: true,
|
||||||
|
linkify: true,
|
||||||
|
});
|
||||||
|
|
||||||
onBeforeMount(() => {
|
onBeforeMount(() => {
|
||||||
console.log("before mount");
|
|
||||||
localStorage.setItem("sidebar_is_collapsed", true);
|
localStorage.setItem("sidebar_is_collapsed", true);
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -73,11 +153,13 @@ const props = defineProps({
|
|||||||
|
|
||||||
const lesson = createResource({
|
const lesson = createResource({
|
||||||
url: "lms.lms.utils.get_lesson",
|
url: "lms.lms.utils.get_lesson",
|
||||||
cache: ["lesson", props.courseName, props.lessonNumber],
|
cache: ["lesson", props.courseName, props.chapterNumber, props.lessonNumber],
|
||||||
params: {
|
makeParams(values) {
|
||||||
course: props.courseName,
|
return {
|
||||||
chapter: props.chapterNumber,
|
course: props.courseName,
|
||||||
lesson: props.lessonNumber,
|
chapter: values ? values.chapter : props.chapterNumber,
|
||||||
|
lesson: values ? values.lesson : props.lessonNumber,
|
||||||
|
}
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
});
|
});
|
||||||
@@ -104,14 +186,32 @@ const breadcrumbs = computed(() => {
|
|||||||
return items
|
return items
|
||||||
});
|
});
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
console.log("unmounted");
|
|
||||||
useStorage("sidebar_is_collapsed", false);
|
useStorage("sidebar_is_collapsed", false);
|
||||||
});
|
});
|
||||||
|
console.log(route.params)
|
||||||
|
watch(
|
||||||
|
[() => route.params.chapterNumber, () => route.params.lessonNumber],
|
||||||
|
([newChapterNumber, newLessonNumber], [oldChapterNumber, oldLessonNumber]) => {
|
||||||
|
lesson.submit({
|
||||||
|
chapter: newChapterNumber,
|
||||||
|
lesson: newLessonNumber,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const getYouTubeVideoSource = (block) => {
|
||||||
|
return `https://www.youtube.com/embed/${getId(block)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getPDFSource = (block) => {
|
||||||
|
return `${getId(block)}#toolbar=0`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getId = (block) => {
|
||||||
|
return block.match(/\(["']([^"']+?)["']\)/)[1];
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
<style>
|
<style>
|
||||||
.youtube-video {
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-group {
|
.avatar-group {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -123,15 +223,50 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
|
border: 1px solid #ddd;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lesson-content div {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.lesson-content p {
|
.lesson-content p {
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
line-height: 1.7;
|
line-height: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.lesson-content li {
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-content ol {
|
||||||
|
list-style: auto;
|
||||||
|
margin: revert;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-content ul {
|
||||||
|
list-style: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: revert;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-content img {
|
||||||
|
border: 1px solid theme("colors.gray.200");
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-content code {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
padding: 1rem 1.25rem;
|
||||||
|
background: #011627;
|
||||||
|
color: #d6deeb;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lesson-content a {
|
||||||
|
color: theme("colors.gray.900");
|
||||||
|
text-decoration: underline;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -742,6 +742,11 @@ engine.io-parser@~5.2.1:
|
|||||||
resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz"
|
resolved "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz"
|
||||||
integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==
|
integrity sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==
|
||||||
|
|
||||||
|
entities@^4.4.0:
|
||||||
|
version "4.5.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48"
|
||||||
|
integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==
|
||||||
|
|
||||||
entities@~3.0.1:
|
entities@~3.0.1:
|
||||||
version "3.0.1"
|
version "3.0.1"
|
||||||
resolved "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz"
|
resolved "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz"
|
||||||
@@ -1109,6 +1114,13 @@ linkify-it@^4.0.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
uc.micro "^1.0.1"
|
uc.micro "^1.0.1"
|
||||||
|
|
||||||
|
linkify-it@^5.0.0:
|
||||||
|
version "5.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-5.0.0.tgz#9ef238bfa6dc70bd8e7f9572b52d369af569b421"
|
||||||
|
integrity sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==
|
||||||
|
dependencies:
|
||||||
|
uc.micro "^2.0.0"
|
||||||
|
|
||||||
linkifyjs@^4.1.0:
|
linkifyjs@^4.1.0:
|
||||||
version "4.1.3"
|
version "4.1.3"
|
||||||
resolved "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz"
|
resolved "https://registry.npmjs.org/linkifyjs/-/linkifyjs-4.1.3.tgz"
|
||||||
@@ -1157,11 +1169,28 @@ markdown-it@^13.0.1:
|
|||||||
mdurl "^1.0.1"
|
mdurl "^1.0.1"
|
||||||
uc.micro "^1.0.5"
|
uc.micro "^1.0.5"
|
||||||
|
|
||||||
|
markdown-it@^14.0.0:
|
||||||
|
version "14.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-14.0.0.tgz#b4b2ddeb0f925e88d981f84c183b59bac9e3741b"
|
||||||
|
integrity sha512-seFjF0FIcPt4P9U39Bq1JYblX0KZCjDLFFQPHpL5AzHpqPEKtosxmdq/LTVZnjfH7tjt9BxStm+wXcDBNuYmzw==
|
||||||
|
dependencies:
|
||||||
|
argparse "^2.0.1"
|
||||||
|
entities "^4.4.0"
|
||||||
|
linkify-it "^5.0.0"
|
||||||
|
mdurl "^2.0.0"
|
||||||
|
punycode.js "^2.3.1"
|
||||||
|
uc.micro "^2.0.0"
|
||||||
|
|
||||||
mdurl@^1.0.1:
|
mdurl@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz"
|
resolved "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz"
|
||||||
integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==
|
integrity sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==
|
||||||
|
|
||||||
|
mdurl@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-2.0.0.tgz#80676ec0433025dd3e17ee983d0fe8de5a2237e0"
|
||||||
|
integrity sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==
|
||||||
|
|
||||||
merge2@^1.3.0:
|
merge2@^1.3.0:
|
||||||
version "1.4.1"
|
version "1.4.1"
|
||||||
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
|
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
|
||||||
@@ -1515,6 +1544,11 @@ prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, pros
|
|||||||
prosemirror-state "^1.0.0"
|
prosemirror-state "^1.0.0"
|
||||||
prosemirror-transform "^1.1.0"
|
prosemirror-transform "^1.1.0"
|
||||||
|
|
||||||
|
punycode.js@^2.3.1:
|
||||||
|
version "2.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/punycode.js/-/punycode.js-2.3.1.tgz#6b53e56ad75588234e79f4affa90972c7dd8cdb7"
|
||||||
|
integrity sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==
|
||||||
|
|
||||||
qalendar@^3.6.1:
|
qalendar@^3.6.1:
|
||||||
version "3.6.1"
|
version "3.6.1"
|
||||||
resolved "https://registry.npmjs.org/qalendar/-/qalendar-3.6.1.tgz"
|
resolved "https://registry.npmjs.org/qalendar/-/qalendar-3.6.1.tgz"
|
||||||
@@ -1702,6 +1736,11 @@ uc.micro@^1.0.1, uc.micro@^1.0.5:
|
|||||||
resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz"
|
resolved "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz"
|
||||||
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
|
integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==
|
||||||
|
|
||||||
|
uc.micro@^2.0.0:
|
||||||
|
version "2.0.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-2.0.0.tgz#84b3c335c12b1497fd9e80fcd3bfa7634c363ff1"
|
||||||
|
integrity sha512-DffL94LsNOccVn4hyfRe5rdKa273swqeA5DJpMOeFmEn1wCDc7nAbbB0gXlgBCL7TNzeTv6G7XVWzan7iJtfig==
|
||||||
|
|
||||||
update-browserslist-db@^1.0.13:
|
update-browserslist-db@^1.0.13:
|
||||||
version "1.0.13"
|
version "1.0.13"
|
||||||
resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz"
|
resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz"
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2023-11-07 10:11:49.126789",
|
"modified": "2023-12-27 12:21:19.289520",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz",
|
"name": "LMS Quiz",
|
||||||
@@ -163,6 +163,15 @@
|
|||||||
"role": "Course Creator",
|
"role": "Course Creator",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "LMS Student",
|
||||||
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"show_title_field_in_link": 1,
|
"show_title_field_in_link": 1,
|
||||||
|
|||||||
@@ -1269,21 +1269,22 @@ def get_course_outline(course):
|
|||||||
"""Returns the course outline."""
|
"""Returns the course outline."""
|
||||||
outline = []
|
outline = []
|
||||||
chapters = frappe.get_all(
|
chapters = frappe.get_all(
|
||||||
"Chapter Reference", {"parent": course}, ["chapter"], order_by="idx"
|
"Chapter Reference", {"parent": course}, ["chapter", "idx"], order_by="idx"
|
||||||
)
|
)
|
||||||
for chapter in chapters:
|
for chapter in chapters:
|
||||||
chapter_details = frappe.db.get_value(
|
chapter_details = frappe.db.get_value(
|
||||||
"Course Chapter",
|
"Course Chapter",
|
||||||
chapter.chapter,
|
chapter.chapter,
|
||||||
["name", "title", "description", "idx"],
|
["name", "title", "description"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
chapter_details["idx"] = chapter.idx
|
||||||
chapter_details.lessons = get_lessons(course, chapter_details)
|
chapter_details.lessons = get_lessons(course, chapter_details)
|
||||||
outline.append(chapter_details)
|
outline.append(chapter_details)
|
||||||
return outline
|
return outline
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_lesson(course, chapter, lesson):
|
def get_lesson(course, chapter, lesson):
|
||||||
chapter_name = frappe.db.get_value(
|
chapter_name = frappe.db.get_value(
|
||||||
"Chapter Reference", {"parent": course, "idx": chapter}, "chapter"
|
"Chapter Reference", {"parent": course, "idx": chapter}, "chapter"
|
||||||
@@ -1291,6 +1292,16 @@ def get_lesson(course, chapter, lesson):
|
|||||||
lesson_name = frappe.db.get_value(
|
lesson_name = frappe.db.get_value(
|
||||||
"Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson"
|
"Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson"
|
||||||
)
|
)
|
||||||
|
lesson_details = frappe.db.get_value(
|
||||||
|
"Course Lesson", lesson_name, ["include_in_preview", "title"], as_dict=1
|
||||||
|
)
|
||||||
|
membership = get_membership(course)
|
||||||
|
if not lesson_details.include_in_preview and not membership:
|
||||||
|
return {
|
||||||
|
"no_preview": 1,
|
||||||
|
"title": lesson_details.title,
|
||||||
|
}
|
||||||
|
|
||||||
lesson_details = frappe.db.get_value(
|
lesson_details = frappe.db.get_value(
|
||||||
"Course Lesson",
|
"Course Lesson",
|
||||||
lesson_name,
|
lesson_name,
|
||||||
|
|||||||
Reference in New Issue
Block a user