@@ -98,7 +98,7 @@ describe("Course Creation", () => {
|
|||||||
|
|
||||||
// View Course
|
// View Course
|
||||||
cy.wait(1000);
|
cy.wait(1000);
|
||||||
cy.visit("/lms");
|
cy.visit("/lms/courses");
|
||||||
cy.closeOnboardingModal();
|
cy.closeOnboardingModal();
|
||||||
|
|
||||||
cy.url().should("include", "/lms/courses");
|
cy.url().should("include", "/lms/courses");
|
||||||
|
|||||||
@@ -365,7 +365,7 @@ const addPrograms = async () => {
|
|||||||
let canAddProgram = await checkIfCanAddProgram()
|
let canAddProgram = await checkIfCanAddProgram()
|
||||||
if (!canAddProgram) return
|
if (!canAddProgram) return
|
||||||
let activeFor = ['Programs', 'ProgramDetail']
|
let activeFor = ['Programs', 'ProgramDetail']
|
||||||
let index = 1
|
let index = 2
|
||||||
|
|
||||||
sidebarLinks.value.splice(index, 0, {
|
sidebarLinks.value.splice(index, 0, {
|
||||||
label: 'Programs',
|
label: 'Programs',
|
||||||
@@ -383,6 +383,15 @@ const checkIfCanAddProgram = async () => {
|
|||||||
return programs.enrolled.length > 0 || programs.published.length > 0
|
return programs.enrolled.length > 0 || programs.published.length > 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const addHome = () => {
|
||||||
|
sidebarLinks.value.unshift({
|
||||||
|
label: 'Home',
|
||||||
|
icon: 'Home',
|
||||||
|
to: 'Home',
|
||||||
|
activeFor: ['Home'],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const openPageModal = (link) => {
|
const openPageModal = (link) => {
|
||||||
showPageModal.value = true
|
showPageModal.value = true
|
||||||
pageToEdit.value = link
|
pageToEdit.value = link
|
||||||
@@ -634,6 +643,7 @@ 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
|
||||||
|
addHome()
|
||||||
addPrograms()
|
addPrograms()
|
||||||
addProgrammingExercises()
|
addProgrammingExercises()
|
||||||
addQuizzes()
|
addQuizzes()
|
||||||
|
|||||||
@@ -208,6 +208,10 @@ const props = defineProps({
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
lessonProgress: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const outline = createResource({
|
const outline = createResource({
|
||||||
@@ -229,6 +233,13 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.lessonProgress,
|
||||||
|
() => {
|
||||||
|
outline.reload()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const deleteLesson = createResource({
|
const deleteLesson = createResource({
|
||||||
url: 'lms.lms.api.delete_lesson',
|
url: 'lms.lms.api.delete_lesson',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
class="flex items-center w-full duration-300 ease-in-out group"
|
class="flex items-center w-full duration-300 ease-in-out group"
|
||||||
:class="isCollapsed ? 'p-1 relative' : 'px-2 py-1'"
|
:class="isCollapsed ? 'p-1 relative' : 'px-2 py-1'"
|
||||||
>
|
>
|
||||||
<Tooltip :text="link.label" placement="right">
|
<Tooltip :text="__(link.label)" placement="right">
|
||||||
<slot name="icon">
|
<slot name="icon">
|
||||||
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
|
||||||
<component
|
<component
|
||||||
|
|||||||
@@ -1,22 +1,24 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div v-if="!forHome || (forHome && upcoming_evals.data?.length)">
|
||||||
<div class="flex items-center justify-between mb-4">
|
<div class="flex items-center justify-between mb-4">
|
||||||
<div class="text-lg text-ink-gray-9 font-semibold">
|
<div class="text-lg text-ink-gray-9 font-semibold">
|
||||||
{{ __('Upcoming Evaluations') }}
|
{{ __('Upcoming Evaluations') }}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
v-if="upcoming_evals.data?.length != evaluationCourses.length"
|
v-if="
|
||||||
|
upcoming_evals.data?.length != evaluationCourses.length && !forHome
|
||||||
|
"
|
||||||
@click="openEvalModal"
|
@click="openEvalModal"
|
||||||
>
|
>
|
||||||
{{ __('Schedule Evaluation') }}
|
{{ __('Schedule Evaluation') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="upcoming_evals.data?.length">
|
<div v-if="upcoming_evals.data?.length">
|
||||||
<div class="grid grid-cols-3 gap-4">
|
<div class="grid gap-4" :class="forHome ? 'grid-cols-2' : 'grid-cols-3'">
|
||||||
<div v-for="evl in upcoming_evals.data">
|
<div v-for="evl in upcoming_evals.data">
|
||||||
<div class="border text-ink-gray-7 rounded-md p-3">
|
<div class="border text-ink-gray-7 rounded-md p-3">
|
||||||
<div class="flex justify-between mb-3">
|
<div class="flex justify-between mb-3">
|
||||||
<span class="font-semibold text-ink-gray-9 leading-5">
|
<span class="text-lg font-semibold text-ink-gray-9 leading-5">
|
||||||
{{ evl.course_title }}
|
{{ evl.course_title }}
|
||||||
</span>
|
</span>
|
||||||
<Menu
|
<Menu
|
||||||
@@ -94,8 +96,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-sm italic text-ink-gray-5">
|
<div v-else class="text-ink-gray-5">
|
||||||
{{ __('Please schedule an evaluation to get certified.') }}
|
{{ __('Schedule an evaluation to get certified.') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<EvaluationModal
|
<EvaluationModal
|
||||||
@@ -122,7 +124,6 @@ import EvaluationModal from '@/components/Modals/EvaluationModal.vue'
|
|||||||
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
|
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
|
||||||
|
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
const user = inject('$user')
|
|
||||||
const showEvalModal = ref(false)
|
const showEvalModal = ref(false)
|
||||||
const app = getCurrentInstance()
|
const app = getCurrentInstance()
|
||||||
const { $dialog } = app.appContext.config.globalProperties
|
const { $dialog } = app.appContext.config.globalProperties
|
||||||
@@ -140,12 +141,15 @@ const props = defineProps({
|
|||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
forHome: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const upcoming_evals = createResource({
|
const upcoming_evals = createResource({
|
||||||
url: 'lms.lms.utils.get_upcoming_evals',
|
url: 'lms.lms.utils.get_upcoming_evals',
|
||||||
params: {
|
params: {
|
||||||
student: user.data.name,
|
|
||||||
courses: props.courses.map((course) => course.course),
|
courses: props.courses.map((course) => course.course),
|
||||||
batch: props.batch,
|
batch: props.batch,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -79,14 +79,14 @@
|
|||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="batch.start_date"
|
v-model="batch.start_date"
|
||||||
:label="__('Start Date')"
|
:label="__('Batch Start Date')"
|
||||||
type="date"
|
type="date"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="batch.end_date"
|
v-model="batch.end_date"
|
||||||
:label="__('End Date')"
|
:label="__('Batch End Date')"
|
||||||
type="date"
|
type="date"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
:required="true"
|
:required="true"
|
||||||
@@ -95,14 +95,14 @@
|
|||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="batch.start_time"
|
v-model="batch.start_time"
|
||||||
:label="__('Start Time')"
|
:label="__('Session Start Time')"
|
||||||
type="time"
|
type="time"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
:required="true"
|
:required="true"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="batch.end_time"
|
v-model="batch.end_time"
|
||||||
:label="__('End Time')"
|
:label="__('Session End Time')"
|
||||||
type="time"
|
type="time"
|
||||||
class="mb-4"
|
class="mb-4"
|
||||||
:required="true"
|
:required="true"
|
||||||
|
|||||||
263
frontend/src/pages/Home/AdminHome.vue
Normal file
263
frontend/src/pages/Home/AdminHome.vue
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="createdCourses.data?.length" class="mt-10">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<span class="font-semibold text-lg">
|
||||||
|
{{ __('Courses Created') }}
|
||||||
|
</span>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'Courses',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-1 text-ink-gray-5 text-xs">
|
||||||
|
<span>
|
||||||
|
{{ __('See all') }}
|
||||||
|
</span>
|
||||||
|
<MoveRight class="size-3 stroke-1.5" />
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||||
|
<router-link
|
||||||
|
v-for="course in createdCourses.data"
|
||||||
|
:to="{ name: 'CourseDetail', params: { courseName: course.name } }"
|
||||||
|
>
|
||||||
|
<CourseCard :course="course" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="createdBatches.data?.length" class="mt-10">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<span class="font-semibold text-lg">
|
||||||
|
{{ __('Upcoming Batches') }}
|
||||||
|
</span>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'Batches',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-1 text-ink-gray-5 text-xs">
|
||||||
|
<span>
|
||||||
|
{{ __('See all') }}
|
||||||
|
</span>
|
||||||
|
<MoveRight class="size-3 stroke-1.5" />
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||||
|
<router-link
|
||||||
|
v-for="batch in createdBatches.data"
|
||||||
|
:to="{ name: 'BatchDetail', params: { batchName: batch.name } }"
|
||||||
|
>
|
||||||
|
<BatchCard :batch="batch" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="!createdCourses.data?.length && !createdBatches.data?.length"
|
||||||
|
class="flex flex-col items-center justify-center mt-60"
|
||||||
|
>
|
||||||
|
<GraduationCap class="size-10 mx-auto stroke-1 text-ink-gray-5" />
|
||||||
|
<div class="text-lg font-semibold text-ink-gray-7 mb-1.5">
|
||||||
|
{{ __('No courses created') }}
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="leading-5 text-base w-full md:w-2/5 text-base text-center text-ink-gray-7"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'There are no courses currently. Create your first course to get started!'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
<router-link
|
||||||
|
:to="{ name: 'CourseForm', params: { courseName: 'new' } }"
|
||||||
|
class="mt-4"
|
||||||
|
>
|
||||||
|
<Button>
|
||||||
|
<template #prefix>
|
||||||
|
<Plus class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Create Course') }}
|
||||||
|
</Button>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-5 mt-10">
|
||||||
|
<div v-if="evals?.data?.length">
|
||||||
|
<div class="font-semibold text-lg mb-3">
|
||||||
|
{{ __('Upcoming Evaluations') }}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||||
|
<div
|
||||||
|
v-for="evaluation in evals?.data"
|
||||||
|
class="border rounded-md p-3 flex flex-col h-full cursor-pointer"
|
||||||
|
@click="redirectToProfile()"
|
||||||
|
>
|
||||||
|
<div class="font-semibold text-ink-gray-9 text-lg mb-1">
|
||||||
|
{{ evaluation.course_title }}
|
||||||
|
</div>
|
||||||
|
<div class="text-ink-gray-7 text-sm">
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<Calendar class="w-4 h-4 stroke-1.5" />
|
||||||
|
<span class="ml-2">
|
||||||
|
{{ dayjs(evaluation.date).format('DD MMMM YYYY') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center mb-2">
|
||||||
|
<Clock class="w-4 h-4 stroke-1.5" />
|
||||||
|
<span class="ml-2">
|
||||||
|
{{ formatTime(evaluation.start_time) }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<GraduationCap class="w-4 h-4 stroke-1.5" />
|
||||||
|
<span class="ml-2">
|
||||||
|
{{ evaluation.member_name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="liveClasses?.data?.length">
|
||||||
|
<div class="font-semibold text-lg mb-3">
|
||||||
|
{{ __('Upcoming Live Classes') }}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||||
|
<div v-for="cls in liveClasses?.data" class="border rounded-md p-3">
|
||||||
|
<div class="font-semibold text-ink-gray-9 text-lg mb-1">
|
||||||
|
{{ cls.title }}
|
||||||
|
</div>
|
||||||
|
<div class="text-ink-gray-7 text-sm leading-5 mb-4">
|
||||||
|
{{ cls.description }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-auto space-y-3 text-ink-gray-7 text-sm">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Calendar class="w-4 h-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ dayjs(cls.date).format('DD MMMM YYYY') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Clock class="w-4 h-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ formatTime(cls.time) }} -
|
||||||
|
{{ dayjs(getClassEnd(cls)).format('HH:mm A') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="canAccessClass(cls)"
|
||||||
|
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="user.data?.is_moderator || user.data?.is_evaluator"
|
||||||
|
:href="cls.start_url"
|
||||||
|
target="_blank"
|
||||||
|
class="cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
|
||||||
|
:class="cls.join_url ? 'w-full' : 'w-1/2'"
|
||||||
|
>
|
||||||
|
<Monitor class="h-4 w-4 stroke-1.5" />
|
||||||
|
{{ __('Start') }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="cls.join_url"
|
||||||
|
target="_blank"
|
||||||
|
class="w-full cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
|
||||||
|
>
|
||||||
|
<Video class="h-4 w-4 stroke-1.5" />
|
||||||
|
{{ __('Join') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
v-else-if="hasClassEnded(cls)"
|
||||||
|
:text="__('This class has ended')"
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<div class="flex items-center space-x-2 text-ink-amber-3 w-fit">
|
||||||
|
<Info class="w-4 h-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ __('Ended') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, createResource, Tooltip } from 'frappe-ui'
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import {
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
GraduationCap,
|
||||||
|
Info,
|
||||||
|
Monitor,
|
||||||
|
MoveRight,
|
||||||
|
Plus,
|
||||||
|
Video,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
import { formatTime } from '@/utils'
|
||||||
|
import CourseCard from '@/components/CourseCard.vue'
|
||||||
|
import BatchCard from '@/components/BatchCard.vue'
|
||||||
|
|
||||||
|
const user = inject<any>('$user')
|
||||||
|
const dayjs = inject<any>('$dayjs')
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
liveClasses?: { data?: any[] }
|
||||||
|
evals?: { data?: any[] }
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const createdCourses = createResource({
|
||||||
|
url: 'lms.lms.utils.get_created_courses',
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const createdBatches = createResource({
|
||||||
|
url: 'lms.lms.utils.get_created_batches',
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const getClassEnd = (cls: { date: string; time: string; duration: number }) => {
|
||||||
|
const classStart = new Date(`${cls.date}T${cls.time}`)
|
||||||
|
return new Date(classStart.getTime() + cls.duration * 60000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canAccessClass = (cls: {
|
||||||
|
date: string
|
||||||
|
time: string
|
||||||
|
duration: number
|
||||||
|
}) => {
|
||||||
|
if (cls.date < dayjs().format('YYYY-MM-DD')) return false
|
||||||
|
if (cls.date > dayjs().format('YYYY-MM-DD')) return false
|
||||||
|
if (hasClassEnded(cls)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasClassEnded = (cls: {
|
||||||
|
date: string
|
||||||
|
time: string
|
||||||
|
duration: number
|
||||||
|
}) => {
|
||||||
|
const classEnd = getClassEnd(cls)
|
||||||
|
const now = new Date()
|
||||||
|
return now > classEnd
|
||||||
|
}
|
||||||
|
|
||||||
|
const redirectToProfile = () => {
|
||||||
|
router.push({
|
||||||
|
name: 'ProfileEvaluationSchedule',
|
||||||
|
params: { username: user.data?.username },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
138
frontend/src/pages/Home/Home.vue
Normal file
138
frontend/src/pages/Home/Home.vue
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
<template>
|
||||||
|
<!-- <header
|
||||||
|
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||||
|
>
|
||||||
|
<Breadcrumbs :items="[{ label: __('Home'), route: { name: 'Home' } }]" />
|
||||||
|
</header> -->
|
||||||
|
<div class="w-full px-5 pt-10 pb-10">
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<div class="space-y-2">
|
||||||
|
<div class="text-xl font-bold">
|
||||||
|
{{ __('Hey') }}, {{ user.data?.full_name }} 👋
|
||||||
|
</div>
|
||||||
|
<div class="text-lg text-ink-gray-6">
|
||||||
|
{{ subtitle }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TabButtons v-if="isAdmin" v-model="currentTab" :buttons="tabs" />
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
@click="showStreakModal = true"
|
||||||
|
class="bg-surface-amber-2 px-2 py-1 rounded-md cursor-pointer"
|
||||||
|
>
|
||||||
|
<span> 🔥 </span>
|
||||||
|
<span>
|
||||||
|
{{ streakInfo.data?.current_streak }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<AdminHome
|
||||||
|
v-if="isAdmin && currentTab === 'instructor'"
|
||||||
|
:liveClasses="adminLiveClasses"
|
||||||
|
:evals="adminEvals"
|
||||||
|
/>
|
||||||
|
<StudentHome v-else :myLiveClasses="myLiveClasses" />
|
||||||
|
</div>
|
||||||
|
<Streak v-model="showStreakModal" :streakInfo="streakInfo" />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, inject, onMounted, ref } from 'vue'
|
||||||
|
import {
|
||||||
|
Breadcrumbs,
|
||||||
|
call,
|
||||||
|
createResource,
|
||||||
|
TabButtons,
|
||||||
|
usePageMeta,
|
||||||
|
} from 'frappe-ui'
|
||||||
|
import { sessionStore } from '@/stores/session'
|
||||||
|
import StudentHome from '@/pages/Home/StudentHome.vue'
|
||||||
|
import AdminHome from '@/pages/Home/AdminHome.vue'
|
||||||
|
import Streak from '@/pages/Home/Streak.vue'
|
||||||
|
|
||||||
|
const user = inject<any>('$user')
|
||||||
|
const { brand } = sessionStore()
|
||||||
|
const evalCount = ref(0)
|
||||||
|
const currentTab = ref<'student' | 'instructor'>('instructor')
|
||||||
|
const showStreakModal = ref(false)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
call('lms.lms.utils.get_upcoming_evals').then((data: any) => {
|
||||||
|
evalCount.value = data.length
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const isAdmin = computed(() => {
|
||||||
|
return (
|
||||||
|
user.data?.is_moderator ||
|
||||||
|
user.data?.is_instructor ||
|
||||||
|
user.data?.is_evaluator
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
const myLiveClasses = createResource({
|
||||||
|
url: 'lms.lms.utils.get_my_live_classes',
|
||||||
|
auto: !isAdmin.value ? true : false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const adminLiveClasses = createResource({
|
||||||
|
url: 'lms.lms.utils.get_admin_live_classes',
|
||||||
|
auto: isAdmin.value ? true : false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const adminEvals = createResource({
|
||||||
|
url: 'lms.lms.utils.get_admin_evals',
|
||||||
|
auto: isAdmin.value ? true : false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const streakInfo = createResource({
|
||||||
|
url: 'lms.lms.utils.get_streak_info',
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const subtitle = computed(() => {
|
||||||
|
if (isAdmin.value) {
|
||||||
|
if (adminLiveClasses.data?.length > 0 && adminEvals.data?.length > 0) {
|
||||||
|
return __(
|
||||||
|
'You have {0} upcoming live classes and {1} evaluations scheduled.'
|
||||||
|
).format(adminLiveClasses.data.length, adminEvals.data.length)
|
||||||
|
} else if (adminLiveClasses.data?.length > 0) {
|
||||||
|
return __('You have {0} upcoming live classes.').format(
|
||||||
|
adminLiveClasses.data.length
|
||||||
|
)
|
||||||
|
} else if (adminEvals.data?.length > 0) {
|
||||||
|
return __('You have {0} evaluations scheduled.').format(
|
||||||
|
adminEvals.data.length
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return __('Manage your courses and batches at a glance')
|
||||||
|
} else {
|
||||||
|
if (myLiveClasses.data?.length > 0 && evalCount.value > 0) {
|
||||||
|
return __(
|
||||||
|
'You have {0} upcoming live classes and {1} evaluations scheduled.'
|
||||||
|
).format(myLiveClasses.data.length, evalCount.value)
|
||||||
|
} else if (myLiveClasses.data?.length > 0) {
|
||||||
|
return __('You have {0} upcoming live classes.').format(
|
||||||
|
myLiveClasses.data.length
|
||||||
|
)
|
||||||
|
} else if (evalCount.value > 0) {
|
||||||
|
return __('You have {0} evaluations scheduled.').format(evalCount.value)
|
||||||
|
}
|
||||||
|
return __('Resume where you left off')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ label: __('Student'), value: 'student' },
|
||||||
|
{ label: __('Instructor'), value: 'instructor' },
|
||||||
|
]
|
||||||
|
|
||||||
|
usePageMeta(() => {
|
||||||
|
return {
|
||||||
|
title: __('Home'),
|
||||||
|
icon: brand.favicon,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
78
frontend/src/pages/Home/Streak.vue
Normal file
78
frontend/src/pages/Home/Streak.vue
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
title: __('Learning Consistency'),
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="text-base">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="text-[30px]">🔥</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div class="text-ink-gray-5 mb-1">
|
||||||
|
{{
|
||||||
|
streakInfo.data?.current_streak < 1
|
||||||
|
? __('You can do better,')
|
||||||
|
: streakInfo.data?.current_streak < 10
|
||||||
|
? __('Keep going,')
|
||||||
|
: __('You rock,')
|
||||||
|
}}
|
||||||
|
{{ __(' you are on a') }}
|
||||||
|
</div>
|
||||||
|
<div class="font-semibold text-xl">
|
||||||
|
{{ streakInfo.data?.current_streak }} {{ __('day streak') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="grid grid-cols-2 bg-surface-gray-1 px-2.5 py-2 rounded-md mt-8"
|
||||||
|
>
|
||||||
|
<div class="space-y-1 border-r border-outline-gray-2 mr-4">
|
||||||
|
<div class="text-ink-gray-6">
|
||||||
|
{{ __('Current Streak') }}
|
||||||
|
</div>
|
||||||
|
<div class="font-semibold text-lg">
|
||||||
|
{{ streakInfo.data?.current_streak }} {{ __('days') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-1">
|
||||||
|
<div class="text-ink-gray-6">
|
||||||
|
{{ __('Longest Streak') }}
|
||||||
|
</div>
|
||||||
|
<div class="font-semibold text-lg">
|
||||||
|
{{ streakInfo.data?.longest_streak }} {{ __('days') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="text-ink-gray-7 border border-outline-gray-1 px-2.5 py-2 rounded-md text-xs leading-5 mt-5"
|
||||||
|
>
|
||||||
|
{{
|
||||||
|
__(
|
||||||
|
'Your learning streak counts the number of days in a row you’ve kept up your learning, whether it’s a lesson, quiz, or assignment. Don’t worry, weekends don’t break your streak.'
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Dialog } from 'frappe-ui'
|
||||||
|
|
||||||
|
const show = defineModel<boolean>({
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
streakInfo: {
|
||||||
|
data: {
|
||||||
|
current_streak: number
|
||||||
|
longest_streak: number
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}>()
|
||||||
|
</script>
|
||||||
195
frontend/src/pages/Home/StudentHome.vue
Normal file
195
frontend/src/pages/Home/StudentHome.vue
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-if="myCourses.data?.length" class="mt-10">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<span class="font-semibold text-lg">
|
||||||
|
{{
|
||||||
|
myCourses.data[0].membership
|
||||||
|
? __('My Courses')
|
||||||
|
: __('Our Popular Courses')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'Courses',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x-1 text-ink-gray-5 text-xs">
|
||||||
|
<span>
|
||||||
|
{{ __('See all') }}
|
||||||
|
</span>
|
||||||
|
<MoveRight class="size-3 stroke-1.5" />
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-5">
|
||||||
|
<router-link
|
||||||
|
v-for="course in myCourses.data"
|
||||||
|
:to="{ name: 'CourseDetail', params: { courseName: course.name } }"
|
||||||
|
>
|
||||||
|
<CourseCard :course="course" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="myBatches.data?.length" class="mt-10">
|
||||||
|
<div class="flex items-center justify-between mb-3">
|
||||||
|
<span class="font-semibold text-lg">
|
||||||
|
{{
|
||||||
|
myBatches.data?.[0].students.includes(user.data?.name)
|
||||||
|
? __('My Batches')
|
||||||
|
: __('Our Upcoming Batches')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'Batches',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="flex items-center space-x- 1 text-ink-gray-5 text-xs">
|
||||||
|
<span>
|
||||||
|
{{ __('See all') }}
|
||||||
|
</span>
|
||||||
|
<MoveRight class="size-3 stroke-1.5" />
|
||||||
|
</span>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-5">
|
||||||
|
<router-link
|
||||||
|
v-for="batch in myBatches.data"
|
||||||
|
:to="{ name: 'BatchDetail', params: { batchName: batch.name } }"
|
||||||
|
>
|
||||||
|
<BatchCard :batch="batch" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-2 gap-5 mt-10">
|
||||||
|
<UpcomingEvaluations :forHome="true" />
|
||||||
|
<div v-if="myLiveClasses.data?.length">
|
||||||
|
<div class="font-semibold text-lg mb-3">
|
||||||
|
{{ __('Upcoming Live Classes') }}
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
||||||
|
<div v-for="cls in myLiveClasses.data" class="border rounded-md p-2">
|
||||||
|
<div class="font-semibold text-ink-gray-9 text-lg mb-1">
|
||||||
|
{{ cls.title }}
|
||||||
|
</div>
|
||||||
|
<div class="text-ink-gray-7 text-sm leading-5 mb-4">
|
||||||
|
{{ cls.description }}
|
||||||
|
</div>
|
||||||
|
<div class="mt-auto space-y-3 text-ink-gray-7 text-sm">
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Calendar class="w-4 h-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ dayjs(cls.date).format('DD MMMM YYYY') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center space-x-2">
|
||||||
|
<Clock class="w-4 h-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ formatTime(cls.time) }} -
|
||||||
|
{{ dayjs(getClassEnd(cls)).format('HH:mm A') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="canAccessClass(cls)"
|
||||||
|
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="user.data?.is_moderator || user.data?.is_evaluator"
|
||||||
|
:href="cls.start_url"
|
||||||
|
target="_blank"
|
||||||
|
class="cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
|
||||||
|
:class="cls.join_url ? 'w-full' : 'w-1/2'"
|
||||||
|
>
|
||||||
|
<Monitor class="h-4 w-4 stroke-1.5" />
|
||||||
|
{{ __('Start') }}
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
:href="cls.join_url"
|
||||||
|
target="_blank"
|
||||||
|
class="w-full cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
|
||||||
|
>
|
||||||
|
<Video class="h-4 w-4 stroke-1.5" />
|
||||||
|
{{ __('Join') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<Tooltip
|
||||||
|
v-else-if="hasClassEnded(cls)"
|
||||||
|
:text="__('This class has ended')"
|
||||||
|
placement="right"
|
||||||
|
>
|
||||||
|
<div class="flex items-center space-x-2 text-ink-amber-3 w-fit">
|
||||||
|
<Info class="w-4 h-4 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ __('Ended') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { inject } from 'vue'
|
||||||
|
import { createResource, Tooltip } from 'frappe-ui'
|
||||||
|
import { formatTime } from '@/utils'
|
||||||
|
import {
|
||||||
|
Calendar,
|
||||||
|
Clock,
|
||||||
|
Info,
|
||||||
|
Monitor,
|
||||||
|
MoveRight,
|
||||||
|
Video,
|
||||||
|
} from 'lucide-vue-next'
|
||||||
|
import CourseCard from '@/components/CourseCard.vue'
|
||||||
|
import BatchCard from '@/components/BatchCard.vue'
|
||||||
|
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
|
||||||
|
|
||||||
|
const dayjs = inject<any>('$dayjs')
|
||||||
|
const user = inject<any>('$user')
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
myLiveClasses: any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const myCourses = createResource({
|
||||||
|
url: 'lms.lms.utils.get_my_courses',
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const myBatches = createResource({
|
||||||
|
url: 'lms.lms.utils.get_my_batches',
|
||||||
|
auto: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
const getClassEnd = (cls: { date: string; time: string; duration: number }) => {
|
||||||
|
const classStart = new Date(`${cls.date}T${cls.time}`)
|
||||||
|
return new Date(classStart.getTime() + cls.duration * 60000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const canAccessClass = (cls: {
|
||||||
|
date: string
|
||||||
|
time: string
|
||||||
|
duration: number
|
||||||
|
}) => {
|
||||||
|
if (cls.date < dayjs().format('YYYY-MM-DD')) return false
|
||||||
|
if (cls.date > dayjs().format('YYYY-MM-DD')) return false
|
||||||
|
if (hasClassEnded(cls)) return false
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasClassEnded = (cls: {
|
||||||
|
date: string
|
||||||
|
time: string
|
||||||
|
duration: number
|
||||||
|
}) => {
|
||||||
|
const classEnd = getClassEnd(cls)
|
||||||
|
const now = new Date()
|
||||||
|
return now > classEnd
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -268,6 +268,7 @@
|
|||||||
:courseName="courseName"
|
:courseName="courseName"
|
||||||
:key="chapterNumber"
|
:key="chapterNumber"
|
||||||
:getProgress="lesson.data.membership ? true : false"
|
:getProgress="lesson.data.membership ? true : false"
|
||||||
|
:lessonProgress="lessonProgress"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,13 +3,11 @@ import { usersStore } from './stores/user'
|
|||||||
import { sessionStore } from './stores/session'
|
import { sessionStore } from './stores/session'
|
||||||
import { useSettings } from './stores/settings'
|
import { useSettings } from './stores/settings'
|
||||||
|
|
||||||
let defaultRoute = '/courses'
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
redirect: {
|
name: 'Home',
|
||||||
name: 'Courses',
|
component: () => import('@/pages/Home/Home.vue'),
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/courses',
|
path: '/courses',
|
||||||
@@ -260,6 +258,8 @@ router.beforeEach(async (to, from, next) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!isLoggedIn) {
|
if (!isLoggedIn) {
|
||||||
|
if (to.name == 'Home') router.push({ name: 'Courses' })
|
||||||
|
|
||||||
await allowGuestAccess.promise
|
await allowGuestAccess.promise
|
||||||
if (!allowGuestAccess.data) {
|
if (!allowGuestAccess.data) {
|
||||||
window.location.href = '/login'
|
window.location.href = '/login'
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export const sessionStore = defineStore('lms-session', () => {
|
|||||||
field: 'livecode_url',
|
field: 'livecode_url',
|
||||||
},
|
},
|
||||||
cache: 'livecodeURL',
|
cache: 'livecodeURL',
|
||||||
auto: true,
|
auto: user.value ? true : false,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { createResource } from 'frappe-ui'
|
|||||||
import { sessionStore } from './session'
|
import { sessionStore } from './session'
|
||||||
|
|
||||||
export const useSettings = defineStore('settings', () => {
|
export const useSettings = defineStore('settings', () => {
|
||||||
const { isLoggedIn } = sessionStore()
|
|
||||||
const isSettingsOpen = ref(false)
|
const isSettingsOpen = ref(false)
|
||||||
const activeTab = ref(null)
|
const activeTab = ref(null)
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ export default defineConfig({
|
|||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
host: '0.0.0.0', // Accept connections from any network interface
|
host: '0.0.0.0', // Accept connections from any network interface
|
||||||
allowedHosts: ['ps', 'fs'], // Explicitly allow this host
|
allowedHosts: ['ps', 'fs', 'home'], // Explicitly allow this host
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
366
lms/lms/utils.py
366
lms/lms/utils.py
@@ -2,6 +2,7 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import razorpay
|
import razorpay
|
||||||
@@ -39,12 +40,12 @@ def slugify(title, used_slugs=None):
|
|||||||
If a list of used slugs is specified, it will make sure the generated slug
|
If a list of used slugs is specified, it will make sure the generated slug
|
||||||
is not one of them.
|
is not one of them.
|
||||||
|
|
||||||
>>> slugify("Hello World!")
|
>>> slugify("Hello World!")
|
||||||
'hello-world'
|
'hello-world'
|
||||||
>>> slugify("Hello World!", ["hello-world"])
|
>>> slugify("Hello World!", ["hello-world"])
|
||||||
'hello-world-2'
|
'hello-world-2'
|
||||||
>>> slugify("Hello World!", ["hello-world", "hello-world-2"])
|
>>> slugify("Hello World!", ["hello-world", "hello-world-2"])
|
||||||
'hello-world-3'
|
'hello-world-3'
|
||||||
"""
|
"""
|
||||||
if not used_slugs:
|
if not used_slugs:
|
||||||
used_slugs = []
|
used_slugs = []
|
||||||
@@ -844,14 +845,22 @@ def get_evaluator(course, batch=None):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_upcoming_evals(student, courses, batch=None):
|
def get_upcoming_evals(courses=None, batch=None):
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
return []
|
||||||
|
|
||||||
|
if not courses:
|
||||||
|
courses = []
|
||||||
|
|
||||||
filters = {
|
filters = {
|
||||||
"member": student,
|
"member": frappe.session.user,
|
||||||
"course": ["in", courses],
|
|
||||||
"date": [">=", frappe.utils.nowdate()],
|
"date": [">=", frappe.utils.nowdate()],
|
||||||
"status": "Upcoming",
|
"status": "Upcoming",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(courses) > 0:
|
||||||
|
filters["course"] = ["in", courses]
|
||||||
|
|
||||||
if batch:
|
if batch:
|
||||||
filters["batch_name"] = batch
|
filters["batch_name"] = batch
|
||||||
|
|
||||||
@@ -1127,7 +1136,7 @@ def get_course_details(course):
|
|||||||
# course_details.is_instructor = is_instructor(course_details.name)
|
# course_details.is_instructor = is_instructor(course_details.name)
|
||||||
if course_details.paid_course or course_details.paid_certificate:
|
if course_details.paid_course or course_details.paid_certificate:
|
||||||
"""course_details.course_price, course_details.currency = check_multicurrency(
|
"""course_details.course_price, course_details.currency = check_multicurrency(
|
||||||
course_details.course_price, course_details.currency, None, course_details.amount_usd
|
course_details.course_price, course_details.currency, None, course_details.amount_usd
|
||||||
)"""
|
)"""
|
||||||
course_details.price = fmt_money(course_details.course_price, 0, course_details.currency)
|
course_details.price = fmt_money(course_details.course_price, 0, course_details.currency)
|
||||||
|
|
||||||
@@ -2133,3 +2142,340 @@ def get_related_courses(course):
|
|||||||
|
|
||||||
def persona_captured():
|
def persona_captured():
|
||||||
frappe.db.set_single_value("LMS Settings", "persona_captured", 1)
|
frappe.db.set_single_value("LMS Settings", "persona_captured", 1)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_my_courses():
|
||||||
|
my_courses = []
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
return my_courses
|
||||||
|
|
||||||
|
courses = get_my_latest_courses()
|
||||||
|
|
||||||
|
if not len(courses):
|
||||||
|
courses = get_featured_home_courses()
|
||||||
|
|
||||||
|
if not len(courses):
|
||||||
|
courses = get_popular_courses()
|
||||||
|
|
||||||
|
for course in courses:
|
||||||
|
my_courses.append(get_course_details(course))
|
||||||
|
|
||||||
|
return my_courses
|
||||||
|
|
||||||
|
|
||||||
|
def get_my_latest_courses():
|
||||||
|
return frappe.get_all(
|
||||||
|
"LMS Enrollment",
|
||||||
|
{
|
||||||
|
"member": frappe.session.user,
|
||||||
|
},
|
||||||
|
order_by="creation desc",
|
||||||
|
limit=3,
|
||||||
|
pluck="course",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_featured_home_courses():
|
||||||
|
return frappe.get_all(
|
||||||
|
"LMS Course",
|
||||||
|
{"published": 1, "featured": 1},
|
||||||
|
order_by="published_on desc",
|
||||||
|
limit=3,
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_popular_courses():
|
||||||
|
return frappe.get_all(
|
||||||
|
"LMS Course",
|
||||||
|
{
|
||||||
|
"published": 1,
|
||||||
|
},
|
||||||
|
order_by="enrollments desc",
|
||||||
|
limit=3,
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_my_batches():
|
||||||
|
my_batches = []
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
return my_batches
|
||||||
|
|
||||||
|
batches = get_my_latest_batches()
|
||||||
|
|
||||||
|
if not len(batches):
|
||||||
|
batches = get_upcoming_batches()
|
||||||
|
|
||||||
|
for batch in batches:
|
||||||
|
batch_details = get_batch_details(batch)
|
||||||
|
if batch_details:
|
||||||
|
my_batches.append(batch_details)
|
||||||
|
|
||||||
|
return my_batches
|
||||||
|
|
||||||
|
|
||||||
|
def get_my_latest_batches():
|
||||||
|
return frappe.get_all(
|
||||||
|
"LMS Batch Enrollment",
|
||||||
|
{
|
||||||
|
"member": frappe.session.user,
|
||||||
|
},
|
||||||
|
order_by="creation desc",
|
||||||
|
limit=4,
|
||||||
|
pluck="batch",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_upcoming_batches():
|
||||||
|
return frappe.get_all(
|
||||||
|
"LMS Batch",
|
||||||
|
{
|
||||||
|
"published": 1,
|
||||||
|
"start_date": [">=", getdate()],
|
||||||
|
},
|
||||||
|
order_by="start_date asc",
|
||||||
|
limit=4,
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_my_live_classes():
|
||||||
|
my_live_classes = []
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
return my_live_classes
|
||||||
|
|
||||||
|
batches = frappe.get_all(
|
||||||
|
"LMS Batch Enrollment",
|
||||||
|
{
|
||||||
|
"member": frappe.session.user,
|
||||||
|
},
|
||||||
|
order_by="creation desc",
|
||||||
|
pluck="batch",
|
||||||
|
)
|
||||||
|
|
||||||
|
live_class_details = frappe.get_all(
|
||||||
|
"LMS Live Class",
|
||||||
|
filters={
|
||||||
|
"date": [">=", getdate()],
|
||||||
|
"batch_name": ["in", batches],
|
||||||
|
},
|
||||||
|
fields=[
|
||||||
|
"name",
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"time",
|
||||||
|
"date",
|
||||||
|
"duration",
|
||||||
|
"attendees",
|
||||||
|
"start_url",
|
||||||
|
"join_url",
|
||||||
|
"owner",
|
||||||
|
],
|
||||||
|
limit=2,
|
||||||
|
order_by="date",
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(live_class_details):
|
||||||
|
for live_class in live_class_details:
|
||||||
|
live_class.course_title = frappe.db.get_value("LMS Course", live_class.course, "title")
|
||||||
|
|
||||||
|
my_live_classes.append(live_class)
|
||||||
|
|
||||||
|
return my_live_classes
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_created_courses():
|
||||||
|
created_courses = []
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
return created_courses
|
||||||
|
|
||||||
|
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||||
|
Course = frappe.qb.DocType("LMS Course")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(CourseInstructor)
|
||||||
|
.join(Course)
|
||||||
|
.on(CourseInstructor.parent == Course.name)
|
||||||
|
.select(Course.name)
|
||||||
|
.where(CourseInstructor.instructor == frappe.session.user)
|
||||||
|
.orderby(Course.published_on, order=frappe.qb.desc)
|
||||||
|
.limit(3)
|
||||||
|
)
|
||||||
|
|
||||||
|
results = query.run(as_dict=True)
|
||||||
|
courses = [row["name"] for row in results]
|
||||||
|
|
||||||
|
for course in courses:
|
||||||
|
course_details = get_course_details(course)
|
||||||
|
created_courses.append(course_details)
|
||||||
|
|
||||||
|
return created_courses
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_created_batches():
|
||||||
|
created_batches = []
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
return created_batches
|
||||||
|
|
||||||
|
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||||
|
Batch = frappe.qb.DocType("LMS Batch")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(CourseInstructor)
|
||||||
|
.join(Batch)
|
||||||
|
.on(CourseInstructor.parent == Batch.name)
|
||||||
|
.select(Batch.name)
|
||||||
|
.where(CourseInstructor.instructor == frappe.session.user)
|
||||||
|
.where(Batch.start_date >= getdate())
|
||||||
|
.orderby(Batch.start_date, order=frappe.qb.asc)
|
||||||
|
.limit(4)
|
||||||
|
)
|
||||||
|
|
||||||
|
results = query.run(as_dict=True)
|
||||||
|
batches = [row["name"] for row in results]
|
||||||
|
|
||||||
|
for batch in batches:
|
||||||
|
batch_details = get_batch_details(batch)
|
||||||
|
created_batches.append(batch_details)
|
||||||
|
|
||||||
|
return created_batches
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_admin_live_classes():
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
return []
|
||||||
|
|
||||||
|
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||||
|
LMSLiveClass = frappe.qb.DocType("LMS Live Class")
|
||||||
|
|
||||||
|
query = (
|
||||||
|
frappe.qb.from_(CourseInstructor)
|
||||||
|
.join(LMSLiveClass)
|
||||||
|
.on(CourseInstructor.parent == LMSLiveClass.batch_name)
|
||||||
|
.select(
|
||||||
|
LMSLiveClass.name,
|
||||||
|
LMSLiveClass.title,
|
||||||
|
LMSLiveClass.description,
|
||||||
|
LMSLiveClass.time,
|
||||||
|
LMSLiveClass.date,
|
||||||
|
LMSLiveClass.duration,
|
||||||
|
LMSLiveClass.attendees,
|
||||||
|
LMSLiveClass.start_url,
|
||||||
|
LMSLiveClass.join_url,
|
||||||
|
LMSLiveClass.owner,
|
||||||
|
)
|
||||||
|
.where(CourseInstructor.instructor == frappe.session.user)
|
||||||
|
.where(LMSLiveClass.date >= getdate())
|
||||||
|
.orderby(LMSLiveClass.date, order=frappe.qb.asc)
|
||||||
|
.limit(4)
|
||||||
|
)
|
||||||
|
results = query.run(as_dict=True)
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_admin_evals():
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
return []
|
||||||
|
|
||||||
|
evals = frappe.get_all(
|
||||||
|
"LMS Certificate Request",
|
||||||
|
{
|
||||||
|
"evaluator": frappe.session.user,
|
||||||
|
"date": [">=", getdate()],
|
||||||
|
},
|
||||||
|
[
|
||||||
|
"name",
|
||||||
|
"date",
|
||||||
|
"start_time",
|
||||||
|
"course",
|
||||||
|
"evaluator",
|
||||||
|
"google_meet_link",
|
||||||
|
"member",
|
||||||
|
"member_name",
|
||||||
|
],
|
||||||
|
limit=4,
|
||||||
|
order_by="date asc",
|
||||||
|
)
|
||||||
|
|
||||||
|
for evaluation in evals:
|
||||||
|
evaluation.course_title = frappe.db.get_value("LMS Course", evaluation.course, "title")
|
||||||
|
|
||||||
|
return evals
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_activity_dates(user):
|
||||||
|
doctypes = [
|
||||||
|
"LMS Course Progress",
|
||||||
|
"LMS Quiz Submission",
|
||||||
|
"LMS Assignment Submission",
|
||||||
|
"LMS Programming Exercise Submission",
|
||||||
|
]
|
||||||
|
|
||||||
|
all_dates = []
|
||||||
|
for dt in doctypes:
|
||||||
|
all_dates.extend(frappe.get_all(dt, {"member": user}, pluck="creation"))
|
||||||
|
|
||||||
|
return sorted({d.date() if hasattr(d, "date") else d for d in all_dates})
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_streaks(all_dates):
|
||||||
|
streak = 0
|
||||||
|
longest_streak = 0
|
||||||
|
prev_day = None
|
||||||
|
|
||||||
|
for d in all_dates:
|
||||||
|
if d.weekday() in (5, 6):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if prev_day:
|
||||||
|
expected = prev_day + timedelta(days=1)
|
||||||
|
while expected.weekday() in (5, 6):
|
||||||
|
expected += timedelta(days=1)
|
||||||
|
|
||||||
|
streak = streak + 1 if d == expected else 1
|
||||||
|
else:
|
||||||
|
streak = 1
|
||||||
|
|
||||||
|
longest_streak = max(longest_streak, streak)
|
||||||
|
prev_day = d
|
||||||
|
|
||||||
|
return streak, longest_streak
|
||||||
|
|
||||||
|
|
||||||
|
def calculate_current_streak(all_dates, streak):
|
||||||
|
if not all_dates:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
last_date = all_dates[-1]
|
||||||
|
today = getdate()
|
||||||
|
|
||||||
|
ref_day = today
|
||||||
|
while ref_day.weekday() in (5, 6):
|
||||||
|
ref_day -= timedelta(days=1)
|
||||||
|
|
||||||
|
if last_date == ref_day or last_date == ref_day - timedelta(days=1):
|
||||||
|
return streak
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_streak_info():
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
return {}
|
||||||
|
|
||||||
|
all_dates = fetch_activity_dates(frappe.session.user)
|
||||||
|
streak, longest_streak = calculate_streaks(all_dates)
|
||||||
|
current_streak = calculate_current_streak(all_dates, streak)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"current_streak": current_streak,
|
||||||
|
"longest_streak": longest_streak,
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
function getLiveCodeOptions() {
|
|
||||||
var START = `
|
|
||||||
import sketch
|
|
||||||
code = open("main.py").read()
|
|
||||||
env = dict(sketch.__dict__)
|
|
||||||
exec(code, env)
|
|
||||||
`;
|
|
||||||
|
|
||||||
var SKETCH = `
|
|
||||||
import json
|
|
||||||
|
|
||||||
def sendmsg(msgtype, function, args):
|
|
||||||
"""Sends a message to the frontend.
|
|
||||||
|
|
||||||
The frontend will receive the specified message whenever
|
|
||||||
this function is called. The frontend can decide to some
|
|
||||||
action on each of these messages.
|
|
||||||
"""
|
|
||||||
msg = dict(msgtype=msgtype, function=function, args=args)
|
|
||||||
print("--MSG--", json.dumps(msg))
|
|
||||||
|
|
||||||
def _draw(func, **kwargs):
|
|
||||||
sendmsg(msgtype="draw", function=func, args=kwargs)
|
|
||||||
|
|
||||||
def circle(x, y, d):
|
|
||||||
"""Draws a circle of diameter d with center (x, y).
|
|
||||||
"""
|
|
||||||
_draw("circle", x=x, y=y, d=d)
|
|
||||||
|
|
||||||
def line(x1, y1, x2, y2):
|
|
||||||
"""Draws a line from point (x1, y1) to point (x2, y2).
|
|
||||||
"""
|
|
||||||
_draw("line", x1=x1, y1=y1, x2=x2, y2=y2)
|
|
||||||
|
|
||||||
def rect(x, y, w, h):
|
|
||||||
"""Draws a rectangle on the canvas.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
x: x coordinate of the top-left corner of the rectangle
|
|
||||||
y: y coordinate of the top-left corner of the rectangle
|
|
||||||
w: width of the rectangle
|
|
||||||
h: height of the rectangle
|
|
||||||
"""
|
|
||||||
_draw("rect", x=x, y=y, w=w, h=h)
|
|
||||||
|
|
||||||
def clear():
|
|
||||||
_draw("clear")
|
|
||||||
|
|
||||||
# clear the canvas on start
|
|
||||||
clear()
|
|
||||||
`;
|
|
||||||
const CANVAS_FUNCTIONS = {
|
|
||||||
circle: function (ctx, args) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(args.x, args.y, args.d / 2, 0, 2 * Math.PI);
|
|
||||||
ctx.stroke();
|
|
||||||
},
|
|
||||||
line: function (ctx, args) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(args.x1, args.y1);
|
|
||||||
ctx.lineTo(args.x2, args.y2);
|
|
||||||
ctx.stroke();
|
|
||||||
},
|
|
||||||
rect: function (ctx, args) {
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.rect(args.x, args.y, args.w, args.h);
|
|
||||||
ctx.stroke();
|
|
||||||
},
|
|
||||||
clear: function (ctx, args) {
|
|
||||||
var width = 300;
|
|
||||||
var height = 300;
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
function drawOnCanvas(canvasElement, funcName, args) {
|
|
||||||
var ctx = canvasElement.getContext("2d");
|
|
||||||
var func = CANVAS_FUNCTIONS[funcName];
|
|
||||||
|
|
||||||
var scalex = canvasElement.width / 300;
|
|
||||||
var scaley = canvasElement.height / 300;
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
ctx.scale(scalex, scaley);
|
|
||||||
func(ctx, args);
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
runtime: "python",
|
|
||||||
files: [
|
|
||||||
{ filename: "start.py", contents: START },
|
|
||||||
{ filename: "sketch.py", contents: SKETCH },
|
|
||||||
],
|
|
||||||
command: ["python", "start.py"],
|
|
||||||
codemirror: true,
|
|
||||||
onMessage: {
|
|
||||||
draw: function (editor, msg) {
|
|
||||||
const canvasElement = editor.parent.querySelector("canvas");
|
|
||||||
drawOnCanvas(canvasElement, msg.function, msg.args);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
frappe.provide("lms.setup");
|
|
||||||
|
|
||||||
// redirect to desk page 'lms' after setup wizard is complete
|
|
||||||
// 'lms' desk page redirects to '/courses'
|
|
||||||
//frappe.setup.welcome_page = "/app/lms-home";
|
|
||||||
Reference in New Issue
Block a user