fix: misc batch issues
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
<template>
|
<template>
|
||||||
<Layout>
|
<FrappeUIProvider>
|
||||||
<router-view />
|
<Layout>
|
||||||
</Layout>
|
<router-view />
|
||||||
<Dialogs />
|
</Layout>
|
||||||
<Toasts />
|
<Dialogs />
|
||||||
|
</FrappeUIProvider>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Toasts } from 'frappe-ui'
|
import { FrappeUIProvider } from 'frappe-ui'
|
||||||
import { Dialogs } from '@/utils/dialogs'
|
import { Dialogs } from '@/utils/dialogs'
|
||||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useScreenSize } from './utils/composables'
|
import { useScreenSize } from './utils/composables'
|
||||||
|
|||||||
@@ -2,7 +2,12 @@
|
|||||||
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
|
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
|
||||||
<div
|
<div
|
||||||
v-if="batch.data.seat_count && seats_left > 0"
|
v-if="batch.data.seat_count && seats_left > 0"
|
||||||
class="text-xs bg-green-100 text-green-700 float-right px-2 py-0.5 rounded-md"
|
class="text-sm bg-green-100 text-green-700 px-2 py-1 rounded-md"
|
||||||
|
:class="
|
||||||
|
batch.data.amount || batch.data.courses.length
|
||||||
|
? 'float-right'
|
||||||
|
: 'w-fit mb-4'
|
||||||
|
"
|
||||||
>
|
>
|
||||||
{{ seats_left }}
|
{{ seats_left }}
|
||||||
<span v-if="seats_left > 1">
|
<span v-if="seats_left > 1">
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
|
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
|
||||||
>
|
>
|
||||||
<ComboboxOptions
|
<ComboboxOptions
|
||||||
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
|
class="my-1 min-h-[6rem] max-h-[12rem] overflow-y-auto px-1.5"
|
||||||
static
|
static
|
||||||
>
|
>
|
||||||
<ComboboxOption
|
<ComboboxOption
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
</Popover>
|
</Popover>
|
||||||
</Combobox>
|
</Combobox>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="values.length" class="grid grid-cols-2 gap-2 mt-4">
|
<div v-if="values.length" class="grid grid-cols-2 gap-2 mt-1">
|
||||||
<div
|
<div
|
||||||
v-for="value in values"
|
v-for="value in values"
|
||||||
class="flex items-center justify-between break-all bg-surface-gray-2 text-ink-gray-7 word-wrap p-2 rounded-md mr-2"
|
class="flex items-center justify-between break-all bg-surface-gray-2 text-ink-gray-7 word-wrap p-2 rounded-md mr-2"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
@change="(val) => (assignment.question = val)"
|
@change="(val) => (assignment.question = val)"
|
||||||
:editable="true"
|
:editable="true"
|
||||||
: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] max-h-[18rem] overflow-y-auto"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ watch(tabIndex, () => {
|
|||||||
|
|
||||||
const canMakeAnnouncement = () => {
|
const canMakeAnnouncement = () => {
|
||||||
if (readOnlyMode) return false
|
if (readOnlyMode) return false
|
||||||
return user.data?.is_moderator || user.data?.is_evaluator
|
if (batch.data) return user.data?.is_moderator || user.data?.is_evaluator
|
||||||
}
|
}
|
||||||
|
|
||||||
usePageMeta(() => {
|
usePageMeta(() => {
|
||||||
|
|||||||
@@ -6,67 +6,45 @@
|
|||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
</header>
|
</header>
|
||||||
<div class="m-5 pb-10">
|
<div class="m-5 pb-10">
|
||||||
<div>
|
<div class="flex justify-between w-full">
|
||||||
<div class="text-3xl font-semibold text-ink-gray-9">
|
<div class="md:w-2/3">
|
||||||
{{ batch.data.title }}
|
<div class="text-3xl font-semibold text-ink-gray-9">
|
||||||
</div>
|
{{ batch.data.title }}
|
||||||
<div class="my-3 leading-6 text-ink-gray-7">
|
|
||||||
{{ batch.data.description }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="flex flex-col gap-2 lg:gap-0 lg:flex-row lg:items-center space-x-0 md:space-x-5 lg:w-1/2"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="batch.data?.courses?.length"
|
|
||||||
class="flex items-center text-ink-gray-7"
|
|
||||||
>
|
|
||||||
<BookOpen class="h-4 w-4 mr-2 stroke-1.5" />
|
|
||||||
<span> {{ batch.data?.courses?.length }} {{ __('Courses') }} </span>
|
|
||||||
</div>
|
</div>
|
||||||
<span v-if="batch.data?.courses?.length" class="hidden lg:block"
|
<div class="my-3 leading-6 text-ink-gray-7">
|
||||||
>·</span
|
{{ batch.data.description }}
|
||||||
>
|
|
||||||
<DateRange
|
|
||||||
:startDate="batch.data.start_date"
|
|
||||||
:endDate="batch.data.end_date"
|
|
||||||
/>
|
|
||||||
<span class="hidden lg:block" v-if="batch.data.start_date"
|
|
||||||
>·</span
|
|
||||||
>
|
|
||||||
<div class="flex items-center text-ink-gray-7">
|
|
||||||
<Clock class="h-4 w-4 mr-2 stroke-1.5" />
|
|
||||||
<span>
|
|
||||||
{{ formatTime(batch.data.start_time) }} -
|
|
||||||
{{ formatTime(batch.data.end_time) }}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex avatar-group overlap">
|
||||||
<div class="flex avatar-group overlap mt-3">
|
<div
|
||||||
<div
|
class="h-6 mr-1"
|
||||||
class="h-6 mr-1"
|
:class="{
|
||||||
:class="{
|
'avatar-group overlap': batch.data.instructors.length > 1,
|
||||||
'avatar-group overlap': batch.data.instructors.length > 1,
|
}"
|
||||||
}"
|
>
|
||||||
>
|
<UserAvatar
|
||||||
<UserAvatar
|
v-for="instructor in batch.data.instructors"
|
||||||
v-for="instructor in batch.data.instructors"
|
:user="instructor"
|
||||||
:user="instructor"
|
/>
|
||||||
/>
|
</div>
|
||||||
|
<CourseInstructors :instructors="batch.data.instructors" />
|
||||||
</div>
|
</div>
|
||||||
<CourseInstructors :instructors="batch.data.instructors" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid lg:grid-cols-[60%,20%] gap-4 lg:gap-20 mt-10">
|
|
||||||
<div class="order-2 lg:order-none">
|
|
||||||
<div
|
<div
|
||||||
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-6"
|
class="ProseMirror prose prose-table:table-fixed prose-td:p-2 prose-th:p-2 prose-td:border prose-th:border prose-td:border-outline-gray-2 prose-th:border-outline-gray-2 prose-td:relative prose-th:relative prose-th:bg-surface-gray-2 prose-sm max-w-none !whitespace-normal mt-10"
|
||||||
v-html="batch.data.batch_details"
|
v-html="batch.data.batch_details"
|
||||||
></div>
|
></div>
|
||||||
|
</div>
|
||||||
|
<div class="hidden md:block">
|
||||||
|
<BatchOverlay :batch="batch" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="grid lg:grid-cols-[60%,20%] gap-4 lg:gap-20 mt-10">
|
||||||
|
<div class="order-2 lg:order-none">
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="order-1 lg:order-none">
|
<div class="order-1 lg:order-none">
|
||||||
<BatchOverlay :batch="batch" />
|
<BatchOverlay :batch="batch" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div> -->
|
||||||
<div v-if="batch.data.courses.length">
|
<div v-if="batch.data.courses.length">
|
||||||
<div class="flex items-center mt-10">
|
<div class="flex items-center mt-10">
|
||||||
<div class="text-2xl font-semibold">
|
<div class="text-2xl font-semibold">
|
||||||
|
|||||||
@@ -21,16 +21,6 @@
|
|||||||
:required="true"
|
:required="true"
|
||||||
class="w-full"
|
class="w-full"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
|
||||||
v-model="batch.description"
|
|
||||||
:label="__('Short Description')"
|
|
||||||
type="textarea"
|
|
||||||
:rows="8"
|
|
||||||
:placeholder="__('Short description of the batch')"
|
|
||||||
:required="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="space-y-5">
|
|
||||||
<MultiSelect
|
<MultiSelect
|
||||||
v-model="instructors"
|
v-model="instructors"
|
||||||
doctype="User"
|
doctype="User"
|
||||||
@@ -39,20 +29,15 @@
|
|||||||
:onCreate="(close) => openSettings('Members', close)"
|
:onCreate="(close) => openSettings('Members', close)"
|
||||||
:filters="{ ignore_user_type: 1 }"
|
:filters="{ ignore_user_type: 1 }"
|
||||||
/>
|
/>
|
||||||
<div>
|
|
||||||
<label class="block text-sm text-ink-gray-5 mb-1">
|
|
||||||
{{ __('Batch Details') }}
|
|
||||||
<span class="text-ink-red-3">*</span>
|
|
||||||
</label>
|
|
||||||
<TextEditor
|
|
||||||
:content="batch.batch_details"
|
|
||||||
@change="(val) => (batch.batch_details = val)"
|
|
||||||
:editable="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] mb-4"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.description"
|
||||||
|
:label="__('Short Description')"
|
||||||
|
type="textarea"
|
||||||
|
:rows="8"
|
||||||
|
:placeholder="__('Short description of the batch')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -135,6 +120,22 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="px-20 pb-5 space-y-5 border-b mb-5">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm text-ink-gray-5 mb-1">
|
||||||
|
{{ __('Batch Details') }}
|
||||||
|
<span class="text-ink-red-3">*</span>
|
||||||
|
</label>
|
||||||
|
<TextEditor
|
||||||
|
:content="batch.batch_details"
|
||||||
|
@change="(val) => (batch.batch_details = val)"
|
||||||
|
:editable="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] max-h-[20rem] overflow-y-scroll mb-4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="px-20 pb-5 space-y-5 border-b mb-5">
|
<div class="px-20 pb-5 space-y-5 border-b mb-5">
|
||||||
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
<div class="text-lg text-ink-gray-9 font-semibold mb-4">
|
||||||
{{ __('Configurations') }}
|
{{ __('Configurations') }}
|
||||||
@@ -236,7 +237,7 @@
|
|||||||
|
|
||||||
<div class="px-20 pb-5 space-y-5">
|
<div class="px-20 pb-5 space-y-5">
|
||||||
<div class="text-lg text-ink-gray-9 font-semibold">
|
<div class="text-lg text-ink-gray-9 font-semibold">
|
||||||
{{ __('Payment') }}
|
{{ __('Pricing') }}
|
||||||
</div>
|
</div>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="batch.paid_batch"
|
v-model="batch.paid_batch"
|
||||||
|
|||||||
@@ -270,6 +270,7 @@ import {
|
|||||||
FormControl,
|
FormControl,
|
||||||
FileUploader,
|
FileUploader,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
|
toast,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import {
|
import {
|
||||||
inject,
|
inject,
|
||||||
@@ -282,7 +283,7 @@ import {
|
|||||||
getCurrentInstance,
|
getCurrentInstance,
|
||||||
} from 'vue'
|
} from 'vue'
|
||||||
import { showToast } from '@/utils'
|
import { showToast } from '@/utils'
|
||||||
import { Image, Trash2, X } from 'lucide-vue-next'
|
import { Check, Image, Trash2, X } from 'lucide-vue-next'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import { useOnboarding } from 'frappe-ui/frappe'
|
import { useOnboarding } from 'frappe-ui/frappe'
|
||||||
@@ -449,7 +450,28 @@ const submitCourse = () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
showToast('Success', 'Course updated successfully', 'check')
|
console.log('Course updated successfully')
|
||||||
|
/* showToast('Success', 'Course updated successfully', 'check') */
|
||||||
|
/* First arg of toast.promise is a promiseToResolve and second is the options obj, how to write this */
|
||||||
|
toast.promise(
|
||||||
|
new Promise((resolve) => {
|
||||||
|
resolve()
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
open: true,
|
||||||
|
type: 'success',
|
||||||
|
message: __('Course updated successfully'),
|
||||||
|
icon: Check,
|
||||||
|
duration: 500,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
/* toast.promise({
|
||||||
|
open: true,
|
||||||
|
type: 'success',
|
||||||
|
message: __('Course updated successfully'),
|
||||||
|
icon: Check,
|
||||||
|
duration: 5
|
||||||
|
}) */
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
showToast('Error', err.messages?.[0] || err, 'x')
|
showToast('Error', err.messages?.[0] || err, 'x')
|
||||||
|
|||||||
@@ -168,6 +168,7 @@
|
|||||||
ignore_user_type: 1,
|
ignore_user_type: 1,
|
||||||
}"
|
}"
|
||||||
:label="__('Program Member')"
|
:label="__('Program Member')"
|
||||||
|
:onCreate="(value, close) => openSettings('Members', close)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
@@ -192,7 +193,8 @@ import { computed, ref } from 'vue'
|
|||||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||||
import { showToast } from '@/utils/'
|
import { showToast } from '@/utils/'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
|
import { useSettings } from '@/stores/settings'
|
||||||
import Draggable from 'vuedraggable'
|
import Draggable from 'vuedraggable'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
|
|
||||||
@@ -202,6 +204,7 @@ const currentForm = ref(null)
|
|||||||
const course = ref(null)
|
const course = ref(null)
|
||||||
const member = ref(null)
|
const member = ref(null)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const settingsStore = useSettings()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
programName: {
|
programName: {
|
||||||
@@ -356,6 +359,12 @@ const memberColumns = computed(() => {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const openSettings = (category, close) => {
|
||||||
|
close()
|
||||||
|
settingsStore.activeTab = category
|
||||||
|
settingsStore.isSettingsOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
const breadbrumbs = computed(() => {
|
const breadbrumbs = computed(() => {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ app_license = "AGPL"
|
|||||||
# include js, css files in header of web template
|
# include js, css files in header of web template
|
||||||
web_include_css = "lms.bundle.css"
|
web_include_css = "lms.bundle.css"
|
||||||
# web_include_css = "/assets/lms/css/lms.css"
|
# web_include_css = "/assets/lms/css/lms.css"
|
||||||
web_include_js = ["website.bundle.js"]
|
web_include_js = []
|
||||||
|
|
||||||
# include custom scss in every website theme (without file extension ".scss")
|
# include custom scss in every website theme (without file extension ".scss")
|
||||||
# website_theme_scss = "lms/public/scss/website"
|
# website_theme_scss = "lms/public/scss/website"
|
||||||
|
|||||||
@@ -21,8 +21,7 @@ from lms.lms.utils import (
|
|||||||
|
|
||||||
class LMSBatch(Document):
|
class LMSBatch(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
if self.seat_count:
|
self.validate_seats_left()
|
||||||
self.validate_seats_left()
|
|
||||||
self.validate_batch_end_date()
|
self.validate_batch_end_date()
|
||||||
self.validate_duplicate_courses()
|
self.validate_duplicate_courses()
|
||||||
self.validate_payments_app()
|
self.validate_payments_app()
|
||||||
@@ -94,6 +93,9 @@ class LMSBatch(Document):
|
|||||||
enrollment.save()
|
enrollment.save()
|
||||||
|
|
||||||
def validate_seats_left(self):
|
def validate_seats_left(self):
|
||||||
|
if cint(self.seat_count) < 0:
|
||||||
|
frappe.throw(_("Seat count cannot be negative."))
|
||||||
|
|
||||||
students = frappe.db.count("LMS Batch Enrollment", {"batch": self.name})
|
students = frappe.db.count("LMS Batch Enrollment", {"batch": self.name})
|
||||||
if cint(self.seat_count) < students:
|
if cint(self.seat_count) < students:
|
||||||
frappe.throw(_("There are no seats available in this batch."))
|
frappe.throw(_("There are no seats available in this batch."))
|
||||||
@@ -208,86 +210,6 @@ def authenticate():
|
|||||||
return response.json()["access_token"]
|
return response.json()["access_token"]
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def create_batch(
|
|
||||||
title,
|
|
||||||
start_date,
|
|
||||||
end_date,
|
|
||||||
description=None,
|
|
||||||
batch_details=None,
|
|
||||||
batch_details_raw=None,
|
|
||||||
meta_image=None,
|
|
||||||
seat_count=0,
|
|
||||||
start_time=None,
|
|
||||||
end_time=None,
|
|
||||||
medium="Online",
|
|
||||||
category=None,
|
|
||||||
paid_batch=0,
|
|
||||||
amount=0,
|
|
||||||
currency=None,
|
|
||||||
amount_usd=0,
|
|
||||||
name=None,
|
|
||||||
published=0,
|
|
||||||
evaluation_end_date=None,
|
|
||||||
):
|
|
||||||
frappe.only_for("Moderator")
|
|
||||||
if name:
|
|
||||||
doc = frappe.get_doc("LMS Batch", name)
|
|
||||||
else:
|
|
||||||
doc = frappe.get_doc({"doctype": "LMS Batch"})
|
|
||||||
|
|
||||||
doc.update(
|
|
||||||
{
|
|
||||||
"title": title,
|
|
||||||
"start_date": start_date,
|
|
||||||
"end_date": end_date,
|
|
||||||
"description": description,
|
|
||||||
"batch_details": batch_details,
|
|
||||||
"batch_details_raw": batch_details_raw,
|
|
||||||
"meta_image": meta_image,
|
|
||||||
"seat_count": seat_count,
|
|
||||||
"start_time": start_time,
|
|
||||||
"end_time": end_time,
|
|
||||||
"medium": medium,
|
|
||||||
"category": category,
|
|
||||||
"paid_batch": paid_batch,
|
|
||||||
"amount": amount,
|
|
||||||
"currency": currency,
|
|
||||||
"amount_usd": amount_usd,
|
|
||||||
"published": published,
|
|
||||||
"evaluation_end_date": evaluation_end_date,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
doc.save()
|
|
||||||
return doc
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def add_course(course, parent, name=None, evaluator=None):
|
|
||||||
frappe.only_for("Moderator")
|
|
||||||
|
|
||||||
if frappe.db.exists("Batch Course", {"course": course, "parent": parent}):
|
|
||||||
frappe.throw(_("Course already added to the batch."))
|
|
||||||
|
|
||||||
if name:
|
|
||||||
doc = frappe.get_doc("Batch Course", name)
|
|
||||||
else:
|
|
||||||
doc = frappe.new_doc("Batch Course")
|
|
||||||
|
|
||||||
doc.update(
|
|
||||||
{
|
|
||||||
"course": course,
|
|
||||||
"evaluator": evaluator,
|
|
||||||
"parent": parent,
|
|
||||||
"parentfield": "courses",
|
|
||||||
"parenttype": "LMS Batch",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
doc.save()
|
|
||||||
|
|
||||||
return doc.name
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_batch_timetable(batch):
|
def get_batch_timetable(batch):
|
||||||
timetable = frappe.get_all(
|
timetable = frappe.get_all(
|
||||||
|
|||||||
@@ -1,450 +0,0 @@
|
|||||||
frappe.ready(() => {
|
|
||||||
setup_file_size();
|
|
||||||
pin_header();
|
|
||||||
|
|
||||||
$(".enroll-in-course").click((e) => {
|
|
||||||
enroll_in_course(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".notify-me").click((e) => {
|
|
||||||
notify_user(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".nav-link").click((e) => {
|
|
||||||
change_hash(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.location.hash) {
|
|
||||||
open_tab();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (window.location.pathname == "/statistics") {
|
|
||||||
generate_graph("New Signups", "#new-signups");
|
|
||||||
generate_graph("Course Enrollments", "#course-enrollments");
|
|
||||||
generate_graph("Lesson Completion", "#lesson-completion");
|
|
||||||
generate_course_completion_graph();
|
|
||||||
}
|
|
||||||
|
|
||||||
expand_the_active_chapter();
|
|
||||||
|
|
||||||
$(".chapter-title")
|
|
||||||
.unbind()
|
|
||||||
.click((e) => {
|
|
||||||
rotate_chapter_icon(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
$(".no-preview").click((e) => {
|
|
||||||
show_no_preview_dialog(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#create-batch").click((e) => {
|
|
||||||
open_batch_dialog(e);
|
|
||||||
});
|
|
||||||
|
|
||||||
$("#course-filter").change((e) => {
|
|
||||||
filter_courses(e);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const pin_header = () => {
|
|
||||||
const el = document.querySelector(".sticky");
|
|
||||||
if (el) {
|
|
||||||
const observer = new IntersectionObserver(
|
|
||||||
([e]) =>
|
|
||||||
e.target.classList.toggle("is-pinned", e.intersectionRatio < 1),
|
|
||||||
{ threshold: [1] }
|
|
||||||
);
|
|
||||||
observer.observe(el);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const setup_file_size = () => {
|
|
||||||
frappe.provide("frappe.form.formatters");
|
|
||||||
frappe.form.formatters.FileSize = file_size;
|
|
||||||
};
|
|
||||||
|
|
||||||
const file_size = (value) => {
|
|
||||||
if (value > 1048576) {
|
|
||||||
value = flt(flt(value) / 1048576, 1) + "M";
|
|
||||||
} else if (value > 1024) {
|
|
||||||
value = flt(flt(value) / 1024, 1) + "K";
|
|
||||||
}
|
|
||||||
return value;
|
|
||||||
};
|
|
||||||
|
|
||||||
const enroll_in_course = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
let course = $(e.currentTarget).attr("data-course");
|
|
||||||
if (frappe.session.user == "Guest") {
|
|
||||||
window.location.href = `/login?redirect-to=/courses/${course}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let batch = $(e.currentTarget).attr("data-batch");
|
|
||||||
batch = batch ? decodeURIComponent(batch) : "";
|
|
||||||
frappe.call({
|
|
||||||
method: "lms.lms.doctype.lms_enrollment.lms_enrollment.create_membership",
|
|
||||||
args: {
|
|
||||||
batch: batch ? batch : "",
|
|
||||||
course: course,
|
|
||||||
},
|
|
||||||
callback: (data) => {
|
|
||||||
if (data.message == "OK") {
|
|
||||||
$(".no-preview-modal").modal("hide");
|
|
||||||
frappe.show_alert(
|
|
||||||
{
|
|
||||||
message: __("Enrolled successfully"),
|
|
||||||
indicator: "green",
|
|
||||||
},
|
|
||||||
3
|
|
||||||
);
|
|
||||||
setTimeout(function () {
|
|
||||||
window.location.href = `/courses/${course}/learn/1.1`;
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const notify_user = (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
var course = decodeURIComponent($("#outline-heading").attr("data-course"));
|
|
||||||
if (frappe.session.user == "Guest") {
|
|
||||||
window.location.href = `/login?redirect-to=/courses/${course}`;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: "lms.lms.doctype.lms_course_interest.lms_course_interest.capture_interest",
|
|
||||||
args: {
|
|
||||||
course: course,
|
|
||||||
},
|
|
||||||
callback: (data) => {
|
|
||||||
$(".no-preview-modal").modal("hide");
|
|
||||||
frappe.show_alert(
|
|
||||||
{
|
|
||||||
message: __(
|
|
||||||
"You have opted to be notified for this course. You will receive an email when the course becomes available."
|
|
||||||
),
|
|
||||||
indicator: "green",
|
|
||||||
},
|
|
||||||
3
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
|
||||||
window.location.reload();
|
|
||||||
}, 3000);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const generate_graph = (chart_name, element, type = "line") => {
|
|
||||||
let date = frappe.datetime;
|
|
||||||
|
|
||||||
frappe.call({
|
|
||||||
method: "lms.lms.utils.get_chart_data",
|
|
||||||
args: {
|
|
||||||
chart_name: chart_name,
|
|
||||||
timespan: "Select Date Range",
|
|
||||||
timegrain: "Daily",
|
|
||||||
from_date: date.add_days(date.get_today(), -30),
|
|
||||||
to_date: date.add_days(date.get_today(), +1),
|
|
||||||
},
|
|
||||||
callback: (data) => {
|
|
||||||
render_chart(data.message, chart_name, element, type);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const render_chart = (data, chart_name, element, type) => {
|
|
||||||
const chart = new frappe.Chart(element, {
|
|
||||||
title: chart_name,
|
|
||||||
data: data,
|
|
||||||
type: type,
|
|
||||||
height: 250,
|
|
||||||
colors: ["#4563f1"],
|
|
||||||
axisOptions: {
|
|
||||||
xIsSeries: 1,
|
|
||||||
},
|
|
||||||
lineOptions: {
|
|
||||||
regionFill: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const generate_course_completion_graph = () => {
|
|
||||||
frappe.call({
|
|
||||||
method: "lms.lms.utils.get_course_completion_data",
|
|
||||||
callback: (data) => {
|
|
||||||
render_chart(
|
|
||||||
data.message,
|
|
||||||
"Course Completion",
|
|
||||||
"#course-completion",
|
|
||||||
"pie"
|
|
||||||
);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const change_hash = (e) => {
|
|
||||||
window.location.hash = $(e.currentTarget).attr("href");
|
|
||||||
};
|
|
||||||
|
|
||||||
const open_tab = () => {
|
|
||||||
$(`a[href="${window.location.hash}"]`).click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const expand_the_first_chapter = () => {
|
|
||||||
let elements = $(".course-home-outline .collapse");
|
|
||||||
elements.each((i, element) => {
|
|
||||||
if (i < 1) {
|
|
||||||
show_section(element);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const expand_the_active_chapter = () => {
|
|
||||||
let selector = $(".course-home-headings.title");
|
|
||||||
|
|
||||||
if (selector.length && $(".course-details-page").length) {
|
|
||||||
expand_for_course_details(selector);
|
|
||||||
} else if ($(".active-lesson").length) {
|
|
||||||
/* For course home page */
|
|
||||||
selector = $(".active-lesson");
|
|
||||||
show_section(selector.parent().parent());
|
|
||||||
} else {
|
|
||||||
/* If no active chapter then exapand the first chapter */
|
|
||||||
expand_the_first_chapter();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const expand_for_course_details = (selector) => {
|
|
||||||
$(".lesson-info").removeClass("active-lesson");
|
|
||||||
$(".lesson-info").each((i, elem) => {
|
|
||||||
if ($(elem).data("lesson") == selector.data("lesson")) {
|
|
||||||
$(elem).addClass("active-lesson");
|
|
||||||
show_section($(elem).parent().parent());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const show_section = (element) => {
|
|
||||||
$(element).addClass("show");
|
|
||||||
$(element)
|
|
||||||
.siblings(".chapter-title")
|
|
||||||
.children(".chapter-icon")
|
|
||||||
.css("transform", "rotate(90deg)");
|
|
||||||
$(element).siblings(".chapter-title").attr("aria-expanded", true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const rotate_chapter_icon = (e) => {
|
|
||||||
let icon = $(e.currentTarget).children(".chapter-icon");
|
|
||||||
if (icon.css("transform") == "none") {
|
|
||||||
icon.css("transform", "rotate(90deg)");
|
|
||||||
} else {
|
|
||||||
icon.css("transform", "none");
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const show_no_preview_dialog = (e) => {
|
|
||||||
$("#no-preview-modal").modal("show");
|
|
||||||
};
|
|
||||||
|
|
||||||
const open_batch_dialog = () => {
|
|
||||||
this.batch_dialog = new frappe.ui.Dialog({
|
|
||||||
title: __("New Batch"),
|
|
||||||
fields: [
|
|
||||||
{
|
|
||||||
fieldtype: "Data",
|
|
||||||
label: __("Title"),
|
|
||||||
fieldname: "title",
|
|
||||||
reqd: 1,
|
|
||||||
default: batch_info && batch_info.title,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Check",
|
|
||||||
label: __("Published"),
|
|
||||||
fieldname: "published",
|
|
||||||
default: batch_info && batch_info.published,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Section Break",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Date",
|
|
||||||
label: __("Start Date"),
|
|
||||||
fieldname: "start_date",
|
|
||||||
reqd: 1,
|
|
||||||
default: batch_info && batch_info.start_date,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Date",
|
|
||||||
label: __("End Date"),
|
|
||||||
fieldname: "end_date",
|
|
||||||
reqd: 1,
|
|
||||||
default: batch_info && batch_info.end_date,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Column Break",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Time",
|
|
||||||
label: __("Start Time"),
|
|
||||||
fieldname: "start_time",
|
|
||||||
default: batch_info && batch_info.start_time,
|
|
||||||
reqd: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Time",
|
|
||||||
label: __("End Time"),
|
|
||||||
fieldname: "end_time",
|
|
||||||
default: batch_info && batch_info.end_time,
|
|
||||||
reqd: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Section Break",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Select",
|
|
||||||
label: __("Medium"),
|
|
||||||
fieldname: "medium",
|
|
||||||
options: ["Online", "Offline"],
|
|
||||||
default: (batch_info && batch_info.medium) || "Online",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Link",
|
|
||||||
label: __("Category"),
|
|
||||||
fieldname: "category",
|
|
||||||
options: "LMS Category",
|
|
||||||
only_select: 1,
|
|
||||||
default: batch_info && batch_info.category,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Column Break",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Int",
|
|
||||||
label: __("Seat Count"),
|
|
||||||
fieldname: "seat_count",
|
|
||||||
default: batch_info && batch_info.seat_count,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Date",
|
|
||||||
label: __("Evaluation End Date"),
|
|
||||||
fieldname: "evaluation_end_date",
|
|
||||||
default: batch_info && batch_info.evaluation_end_date,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Section Break",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Small Text",
|
|
||||||
label: __("Description"),
|
|
||||||
fieldname: "description",
|
|
||||||
default: batch_info && batch_info.description,
|
|
||||||
reqd: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Text Editor",
|
|
||||||
label: __("Batch Details"),
|
|
||||||
fieldname: "batch_details",
|
|
||||||
default: batch_info && batch_info.batch_details,
|
|
||||||
reqd: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "HTML Editor",
|
|
||||||
label: __("Batch Details Raw"),
|
|
||||||
fieldname: "batch_details_raw",
|
|
||||||
default: batch_info && batch_info.batch_details_raw,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Attach Image",
|
|
||||||
label: __("Meta Image"),
|
|
||||||
fieldname: "meta_image",
|
|
||||||
default: batch_info && batch_info.meta_image,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Section Break",
|
|
||||||
label: __("Pricing"),
|
|
||||||
fieldname: "pricing",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Check",
|
|
||||||
label: __("Paid Batch"),
|
|
||||||
fieldname: "paid_batch",
|
|
||||||
default: batch_info && batch_info.paid_batch,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Currency",
|
|
||||||
label: __("Amount"),
|
|
||||||
fieldname: "amount",
|
|
||||||
default: batch_info && batch_info.amount,
|
|
||||||
mandatory_depends_on: "paid_batch",
|
|
||||||
depends_on: "paid_batch",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Link",
|
|
||||||
label: __("Currency"),
|
|
||||||
fieldname: "currency",
|
|
||||||
options: "Currency",
|
|
||||||
default: batch_info && batch_info.currency,
|
|
||||||
mandatory_depends_on: "paid_batch",
|
|
||||||
depends_on: "paid_batch",
|
|
||||||
only_select: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
fieldtype: "Currency",
|
|
||||||
label: __("Amount (USD)"),
|
|
||||||
fieldname: "amount_usd",
|
|
||||||
depends_on: "paid_batch",
|
|
||||||
description: __(
|
|
||||||
"If you set an amount here, then the USD equivalent setting will not get applied."
|
|
||||||
),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
primary_action_label: __("Save"),
|
|
||||||
primary_action: (values) => {
|
|
||||||
save_batch(values);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
this.batch_dialog.show();
|
|
||||||
};
|
|
||||||
|
|
||||||
const save_batch = (values) => {
|
|
||||||
let args = {};
|
|
||||||
if (batch_info) {
|
|
||||||
args = Object.assign(batch_info, values);
|
|
||||||
} else {
|
|
||||||
args = values;
|
|
||||||
}
|
|
||||||
frappe.call({
|
|
||||||
method: "lms.lms.doctype.lms_batch.lms_batch.create_batch",
|
|
||||||
args: args,
|
|
||||||
callback: (r) => {
|
|
||||||
if (r.message) {
|
|
||||||
frappe.show_alert({
|
|
||||||
message: batch_info
|
|
||||||
? __("Batch Updated")
|
|
||||||
: __("Batch Created"),
|
|
||||||
indicator: "green",
|
|
||||||
});
|
|
||||||
this.batch_dialog.hide();
|
|
||||||
window.location.href = `/batches/details/${r.message.name}`;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const filter_courses = (e) => {
|
|
||||||
const course_lists = $(".course-cards-parent");
|
|
||||||
const filter = $(e.currentTarget).val();
|
|
||||||
course_lists.each((i, list) => {
|
|
||||||
const course_cards = $(list).children(".course-card");
|
|
||||||
course_cards.sort((a, b) => {
|
|
||||||
var value1 = $(a).data(filter);
|
|
||||||
var value2 = $(b).data(filter);
|
|
||||||
return value1 > value2 ? -1 : value1 < value2 ? 1 : 0;
|
|
||||||
});
|
|
||||||
$(list).append(course_cards);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
frappe.ready(() => {
|
|
||||||
hide_profile_and_dashboard_for_guest_users();
|
|
||||||
});
|
|
||||||
|
|
||||||
const hide_profile_and_dashboard_for_guest_users = () => {
|
|
||||||
if (frappe.session.user == "Guest") {
|
|
||||||
let links = $(".nav-link").filter(
|
|
||||||
(i, elem) =>
|
|
||||||
$(elem).text().trim() === "My Profile" ||
|
|
||||||
$(elem).text().trim() === "Dashboard"
|
|
||||||
);
|
|
||||||
links.length && links.each((i, elem) => $(elem).addClass("hide"));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import "./profile.js";
|
|
||||||
import "./common_functions.js";
|
|
||||||
import "../../../../frappe/frappe/public/js/frappe/ui/chart.js";
|
|
||||||
import "../../../../frappe/frappe/public/js/frappe/ui/keyboard.js";
|
|
||||||
import "../../../../frappe/frappe/public/js/frappe/event_emitter.js";
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{% extends "templates/base.html" %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% include "public/icons/symbol-defs.svg" %}
|
|
||||||
{% include "lms/templates/onboarding_header.html" %}
|
|
||||||
{% block page_content %}
|
|
||||||
Hello, world!
|
|
||||||
{% endblock %}
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{%- block script -%}
|
|
||||||
{{ super() }}
|
|
||||||
{% if frappe.get_system_settings("enable_telemetry") %}
|
|
||||||
{% set telemetry_boot_info = get_telemetry_boot_info() %}
|
|
||||||
<script>
|
|
||||||
const telemetry_boot_info = {{ get_telemetry_boot_info() }}
|
|
||||||
if (telemetry_boot_info && Object.keys(telemetry_boot_info).length)
|
|
||||||
Object.assign(frappe.boot, telemetry_boot_info)
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
<script>
|
|
||||||
frappe.router = {
|
|
||||||
slug(name) {
|
|
||||||
return name.toLowerCase().replace(/ /g, "-");
|
|
||||||
},
|
|
||||||
};
|
|
||||||
frappe.utils.make_event_emitter(frappe.router)
|
|
||||||
</script>
|
|
||||||
{{ include_script("telemetry.bundle.js") }}
|
|
||||||
|
|
||||||
{%- endblock -%}
|
|
||||||
Reference in New Issue
Block a user