chore: capture more events for analytics
(cherry picked from commit b3c8fbd833)
# Conflicts:
# frontend/src/pages/Batches/components/NewBatchModal.vue
# lms/lms/doctype/lms_course_review/lms_course_review.json
This commit is contained in:
@@ -93,11 +93,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createResource, TextEditor, Button, Dropdown, toast } from 'frappe-ui'
|
import {
|
||||||
|
call,
|
||||||
|
createResource,
|
||||||
|
TextEditor,
|
||||||
|
Button,
|
||||||
|
Dropdown,
|
||||||
|
toast,
|
||||||
|
} from 'frappe-ui'
|
||||||
import { timeAgo } from '@/utils'
|
import { timeAgo } from '@/utils'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
|
import { ChevronLeft, MoreHorizontal } from 'lucide-vue-next'
|
||||||
import { ref, inject, onMounted, onUnmounted } from 'vue'
|
import { ref, inject, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useTelemetry } from 'frappe-ui/frappe'
|
||||||
|
|
||||||
const showTopics = defineModel('showTopics')
|
const showTopics = defineModel('showTopics')
|
||||||
const newReply = ref('')
|
const newReply = ref('')
|
||||||
@@ -107,6 +115,7 @@ const allUsers = inject('$allUsers')
|
|||||||
const mentionUsers = ref([])
|
const mentionUsers = ref([])
|
||||||
const renderEditor = ref(false)
|
const renderEditor = ref(false)
|
||||||
const readOnlyMode = window.read_only_mode
|
const readOnlyMode = window.read_only_mode
|
||||||
|
const { capture } = useTelemetry()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
topic: {
|
topic: {
|
||||||
@@ -143,19 +152,6 @@ const replies = createResource({
|
|||||||
auto: true,
|
auto: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
const newReplyResource = createResource({
|
|
||||||
url: 'frappe.client.insert',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doc: {
|
|
||||||
doctype: 'Discussion Reply',
|
|
||||||
reply: newReply.value,
|
|
||||||
topic: props.topic.name,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const fetchMentionUsers = () => {
|
const fetchMentionUsers = () => {
|
||||||
if (user.data?.is_student) {
|
if (user.data?.is_student) {
|
||||||
renderEditor.value = true
|
renderEditor.value = true
|
||||||
@@ -178,78 +174,61 @@ const fetchMentionUsers = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const postReply = () => {
|
const postReply = () => {
|
||||||
newReplyResource.submit(
|
if (!newReply.value) {
|
||||||
{},
|
toast.error(__('Reply cannot be empty.'))
|
||||||
{
|
return
|
||||||
validate() {
|
}
|
||||||
if (!newReply.value) {
|
call('frappe.client.insert', {
|
||||||
return 'Reply cannot be empty'
|
doc: {
|
||||||
}
|
|
||||||
},
|
|
||||||
onSuccess() {
|
|
||||||
newReply.value = ''
|
|
||||||
replies.reload()
|
|
||||||
},
|
|
||||||
onError(err) {
|
|
||||||
toast.error(err.messages?.[0] || err)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const editReplyResource = createResource({
|
|
||||||
url: 'frappe.client.set_value',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doctype: 'Discussion Reply',
|
doctype: 'Discussion Reply',
|
||||||
name: values.name,
|
reply: newReply.value,
|
||||||
fieldname: 'reply',
|
topic: props.topic.name,
|
||||||
value: values.reply,
|
},
|
||||||
}
|
})
|
||||||
},
|
.then((data) => {
|
||||||
})
|
newReply.value = ''
|
||||||
|
replies.reload()
|
||||||
|
capture('discussion_reply_created')
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.messages?.[0] || err)
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const postEdited = (reply) => {
|
const postEdited = (reply) => {
|
||||||
editReplyResource.submit(
|
if (!reply.reply) {
|
||||||
{
|
toast.error(__('Reply cannot be empty.'))
|
||||||
name: reply.name,
|
return
|
||||||
reply: reply.reply,
|
}
|
||||||
},
|
call('frappe.client.set_value', {
|
||||||
{
|
doctype: 'Discussion Reply',
|
||||||
validate() {
|
name: reply.name,
|
||||||
if (!reply.reply) {
|
fieldname: 'reply',
|
||||||
return 'Reply cannot be empty'
|
value: reply.reply,
|
||||||
}
|
})
|
||||||
},
|
.then(() => {
|
||||||
onSuccess() {
|
reply.editable = false
|
||||||
reply.editable = false
|
replies.reload()
|
||||||
replies.reload()
|
})
|
||||||
},
|
.catch((err) => {
|
||||||
}
|
toast.error(err.messages?.[0] || err)
|
||||||
)
|
console.error(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteReplyResource = createResource({
|
|
||||||
url: 'frappe.client.delete',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doctype: 'Discussion Reply',
|
|
||||||
name: values.name,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const deleteReply = (reply) => {
|
const deleteReply = (reply) => {
|
||||||
deleteReplyResource.submit(
|
call('frappe.client.delete', {
|
||||||
{
|
doctype: 'Discussion Reply',
|
||||||
name: reply.name,
|
name: reply.name,
|
||||||
},
|
})
|
||||||
{
|
.then(() => {
|
||||||
onSuccess() {
|
replies.reload()
|
||||||
replies.reload()
|
})
|
||||||
},
|
.catch((err) => {
|
||||||
}
|
toast.error(err.messages?.[0] || err)
|
||||||
)
|
console.error(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
|||||||
@@ -34,17 +34,13 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import { call, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
|
||||||
Dialog,
|
|
||||||
FormControl,
|
|
||||||
TextEditor,
|
|
||||||
createResource,
|
|
||||||
toast,
|
|
||||||
} from 'frappe-ui'
|
|
||||||
import { reactive } from 'vue'
|
import { reactive } from 'vue'
|
||||||
import { singularize } from '@/utils'
|
import { singularize } from '@/utils'
|
||||||
|
import { useTelemetry } from 'frappe-ui/frappe'
|
||||||
|
|
||||||
const topics = defineModel('reloadTopics')
|
const topics = defineModel('reloadTopics')
|
||||||
|
const { capture } = useTelemetry()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
title: {
|
title: {
|
||||||
@@ -66,64 +62,50 @@ const topic = reactive({
|
|||||||
reply: '',
|
reply: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const topicResource = createResource({
|
|
||||||
url: 'frappe.client.insert',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doc: {
|
|
||||||
doctype: 'Discussion Topic',
|
|
||||||
reference_doctype: props.doctype,
|
|
||||||
reference_docname: props.docname,
|
|
||||||
title: topic.title,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const replyResource = createResource({
|
|
||||||
url: 'frappe.client.insert',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doc: {
|
|
||||||
doctype: 'Discussion Reply',
|
|
||||||
topic: values.topic,
|
|
||||||
reply: topic.reply,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const submitTopic = (close) => {
|
const submitTopic = (close) => {
|
||||||
topicResource.submit(
|
if (!topic.title) {
|
||||||
{},
|
toast.error(__('Title cannot be empty.'))
|
||||||
{
|
return
|
||||||
validate() {
|
}
|
||||||
if (!topic.title) {
|
if (!topic.reply) {
|
||||||
return 'Title cannot be empty.'
|
toast.error(__('Details cannot be empty.'))
|
||||||
}
|
return
|
||||||
if (!topic.reply) {
|
}
|
||||||
return 'Reply cannot be empty.'
|
call('frappe.client.insert', {
|
||||||
}
|
doc: {
|
||||||
},
|
doctype: 'Discussion Topic',
|
||||||
onSuccess(data) {
|
reference_doctype: props.doctype,
|
||||||
replyResource.submit(
|
reference_docname: props.docname,
|
||||||
{
|
title: topic.title,
|
||||||
topic: data.name,
|
},
|
||||||
},
|
})
|
||||||
{
|
.then((data) => {
|
||||||
onSuccess() {
|
createReply(data.name, close)
|
||||||
topic.title = ''
|
})
|
||||||
topic.reply = ''
|
.catch((err) => {
|
||||||
topics.value.reload()
|
toast.error(err.messages?.[0] || err)
|
||||||
close()
|
console.error(err)
|
||||||
},
|
})
|
||||||
}
|
}
|
||||||
)
|
|
||||||
},
|
const createReply = (topicName, close) => {
|
||||||
onError(err) {
|
call('frappe.client.insert', {
|
||||||
toast.error(err.messages?.[0] || err)
|
doc: {
|
||||||
},
|
doctype: 'Discussion Reply',
|
||||||
}
|
topic: topicName,
|
||||||
)
|
reply: topic.reply,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
topic.title = ''
|
||||||
|
topic.reply = ''
|
||||||
|
topics.value.reload()
|
||||||
|
capture('discussion_topic_created')
|
||||||
|
close()
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
toast.error(err.messages?.[0] || err)
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ import { ref, watch, reactive, inject } from 'vue'
|
|||||||
import { RefreshCw, Plus, Search, Shield } from 'lucide-vue-next'
|
import { RefreshCw, Plus, Search, Shield } from 'lucide-vue-next'
|
||||||
import { useOnboarding } from 'frappe-ui/frappe'
|
import { useOnboarding } from 'frappe-ui/frappe'
|
||||||
import type { User } from '@/components/Settings/types'
|
import type { User } from '@/components/Settings/types'
|
||||||
|
import { useTelemetry } from 'frappe-ui/frappe'
|
||||||
|
|
||||||
type Member = {
|
type Member = {
|
||||||
username: string
|
username: string
|
||||||
@@ -149,6 +150,7 @@ const hasNextPage = ref(false)
|
|||||||
const showForm = ref(false)
|
const showForm = ref(false)
|
||||||
const user = inject<User | null>('$user')
|
const user = inject<User | null>('$user')
|
||||||
const { updateOnboardingStep } = useOnboarding('learning')
|
const { updateOnboardingStep } = useOnboarding('learning')
|
||||||
|
const { capture } = useTelemetry()
|
||||||
|
|
||||||
const member = reactive({
|
const member = reactive({
|
||||||
email: '',
|
email: '',
|
||||||
@@ -202,6 +204,7 @@ const addMember = (close: () => void) => {
|
|||||||
})
|
})
|
||||||
.then((data: Member) => {
|
.then((data: Member) => {
|
||||||
if (user?.data?.is_system_manager) updateOnboardingStep('invite_students')
|
if (user?.data?.is_system_manager) updateOnboardingStep('invite_students')
|
||||||
|
capture('user_added')
|
||||||
show.value = false
|
show.value = false
|
||||||
router.push({
|
router.push({
|
||||||
name: 'ProfileRoles',
|
name: 'ProfileRoles',
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
<template>
|
||||||
|
<Dialog
|
||||||
|
v-model="show"
|
||||||
|
:options="{
|
||||||
|
title: __('New Batch'),
|
||||||
|
size: '3xl',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<template #body-content>
|
||||||
|
<div class="text-base">
|
||||||
|
<div class="grid grid-cols-2 gap-10">
|
||||||
|
<div class="space-y-5">
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.title"
|
||||||
|
:label="__('Title')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.start_date"
|
||||||
|
:label="__('Start Date')"
|
||||||
|
type="date"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.end_date"
|
||||||
|
:label="__('End Date')"
|
||||||
|
type="date"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<Link
|
||||||
|
doctype="LMS Category"
|
||||||
|
v-model="batch.category"
|
||||||
|
:label="__('Category')"
|
||||||
|
:allowCreate="true"
|
||||||
|
@create="
|
||||||
|
() => {
|
||||||
|
openSettings('Categories')
|
||||||
|
show = false
|
||||||
|
}
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="space-y-5">
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.start_time"
|
||||||
|
:label="__('Start Time')"
|
||||||
|
type="time"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.end_time"
|
||||||
|
:label="__('End Time')"
|
||||||
|
type="time"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.timezone"
|
||||||
|
:label="__('Timezone')"
|
||||||
|
:required="true"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.seat_count"
|
||||||
|
:label="__('Seat Count')"
|
||||||
|
type="number"
|
||||||
|
:required="false"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="space-y-5 border-t mt-5 pt-5">
|
||||||
|
<MultiSelect
|
||||||
|
v-model="batch.instructors"
|
||||||
|
doctype="Course Evaluator"
|
||||||
|
:label="__('Instructors')"
|
||||||
|
:required="true"
|
||||||
|
:onCreate="(close: () => void) => openSettings('Evaluators', close)"
|
||||||
|
:filters="{ ignore_user_type: 1 }"
|
||||||
|
/>
|
||||||
|
<FormControl
|
||||||
|
v-model="batch.description"
|
||||||
|
:label="__('Description')"
|
||||||
|
type="textarea"
|
||||||
|
:required="true"
|
||||||
|
:rows="4"
|
||||||
|
/>
|
||||||
|
<div class="">
|
||||||
|
<div class="mb-1.5 text-sm text-ink-gray-5">
|
||||||
|
{{ __('Batch Details') }}
|
||||||
|
<span class="text-ink-red-3">*</span>
|
||||||
|
</div>
|
||||||
|
<TextEditor
|
||||||
|
:content="batch.batch_details"
|
||||||
|
@change="(val: string) => (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-[10rem]"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template #actions="{ close }">
|
||||||
|
<div class="text-right">
|
||||||
|
<Button variant="solid" @click="saveBatch(close)">
|
||||||
|
{{ __('Save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
|
||||||
|
import { Link, useOnboarding, useTelemetry } from 'frappe-ui/frappe'
|
||||||
|
import { ref, inject, onMounted, onBeforeUnmount } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { cleanError, openSettings } from '@/utils'
|
||||||
|
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||||
|
|
||||||
|
const show = defineModel<boolean>({ required: true, default: false })
|
||||||
|
const router = useRouter()
|
||||||
|
const { capture } = useTelemetry()
|
||||||
|
const { updateOnboardingStep } = useOnboarding('learning')
|
||||||
|
const user = inject<any>('$user')
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
batches: any
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const batch = ref({
|
||||||
|
title: '',
|
||||||
|
start_date: null,
|
||||||
|
end_date: null,
|
||||||
|
start_time: null,
|
||||||
|
end_time: null,
|
||||||
|
timezone: null,
|
||||||
|
description: '',
|
||||||
|
batch_details: '',
|
||||||
|
instructors: [],
|
||||||
|
category: null,
|
||||||
|
seat_count: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveBatch = (close: () => void = () => {}) => {
|
||||||
|
props.batches.insert.submit(
|
||||||
|
{
|
||||||
|
...batch.value,
|
||||||
|
instructors: batch.value.instructors.map((instructor) => ({
|
||||||
|
instructor: instructor,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
onSuccess(data: any) {
|
||||||
|
toast.success(__('Batch created successfully'))
|
||||||
|
close()
|
||||||
|
capture('batch_created')
|
||||||
|
router.push({
|
||||||
|
name: 'BatchDetail',
|
||||||
|
params: { batchName: data.name },
|
||||||
|
hash: '#settings',
|
||||||
|
})
|
||||||
|
if (user.data?.is_system_manager) {
|
||||||
|
updateOnboardingStep('create_first_batch', true, false, () => {
|
||||||
|
localStorage.setItem('firstBatch', data.name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onError(err: any) {
|
||||||
|
toast.error(cleanError(err.messages?.[0]))
|
||||||
|
console.error(err)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyboardShortcut = (e: KeyboardEvent) => {
|
||||||
|
if (
|
||||||
|
e.key === 's' &&
|
||||||
|
(e.ctrlKey || e.metaKey) &&
|
||||||
|
e.target &&
|
||||||
|
e.target instanceof HTMLElement &&
|
||||||
|
!e.target.classList.contains('ProseMirror')
|
||||||
|
) {
|
||||||
|
saveBatch()
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
window.addEventListener('keydown', keyboardShortcut)
|
||||||
|
capture('batch_form_opened')
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('keydown', keyboardShortcut)
|
||||||
|
capture('batch_form_closed', {
|
||||||
|
data: batch.value,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -76,7 +76,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
|
import { Button, Dialog, FormControl, TextEditor, toast } from 'frappe-ui'
|
||||||
import { Link, useOnboarding, useTelemetry } from 'frappe-ui/frappe'
|
import { Link, useOnboarding, useTelemetry } from 'frappe-ui/frappe'
|
||||||
import { inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
import { inject, onMounted, onBeforeUnmount, ref } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { cleanError, openSettings } from '@/utils'
|
import { cleanError, openSettings } from '@/utils'
|
||||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||||
@@ -148,13 +148,13 @@ const keyboardShortcut = (e: KeyboardEvent) => {
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
window.addEventListener('keydown', keyboardShortcut)
|
window.addEventListener('keydown', keyboardShortcut)
|
||||||
|
capture('course_form_opened')
|
||||||
})
|
})
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
window.removeEventListener('keydown', keyboardShortcut)
|
window.removeEventListener('keydown', keyboardShortcut)
|
||||||
})
|
capture('course_form_closed', {
|
||||||
|
data: course.value,
|
||||||
watch(show, () => {
|
})
|
||||||
if (show.value) capture('course_form_opened')
|
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ def save_progress(lesson: str, course: str, scorm_details: dict = None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
progress = get_course_progress(course)
|
progress = get_course_progress(course)
|
||||||
capture_progress_for_analytics(progress, course)
|
capture_progress_for_analytics()
|
||||||
|
|
||||||
# Had to get doc, as on_change doesn't trigger when you use set_value. The trigger is necessary for badge to get assigned.
|
# Had to get doc, as on_change doesn't trigger when you use set_value. The trigger is necessary for badge to get assigned.
|
||||||
enrollment = frappe.get_doc("LMS Enrollment", membership)
|
enrollment = frappe.get_doc("LMS Enrollment", membership)
|
||||||
@@ -121,9 +121,8 @@ def save_progress(lesson: str, course: str, scorm_details: dict = None):
|
|||||||
return progress
|
return progress
|
||||||
|
|
||||||
|
|
||||||
def capture_progress_for_analytics(progress, course):
|
def capture_progress_for_analytics():
|
||||||
if progress in [25, 50, 75, 100]:
|
capture("course_progress", "lms")
|
||||||
capture("course_progress", "lms", properties={"course": course, "progress": progress})
|
|
||||||
|
|
||||||
|
|
||||||
def get_quiz_progress(lesson):
|
def get_quiz_progress(lesson):
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ class LMSCertificate(Document):
|
|||||||
self.name = make_autoname("hash", self.doctype)
|
self.name = make_autoname("hash", self.doctype)
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
self.send_certification_email()
|
|
||||||
capture("certificate_issued", "lms")
|
capture("certificate_issued", "lms")
|
||||||
|
self.send_certification_email()
|
||||||
|
|
||||||
def send_certification_email(self):
|
def send_certification_email(self):
|
||||||
outgoing_email_account = frappe.get_cached_value(
|
outgoing_email_account = frappe.get_cached_value(
|
||||||
|
|||||||
@@ -41,7 +41,11 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
|
<<<<<<< HEAD
|
||||||
"modified": "2026-01-29 16:10:47.787285",
|
"modified": "2026-01-29 16:10:47.787285",
|
||||||
|
=======
|
||||||
|
"modified": "2026-02-23 16:21:18.503806",
|
||||||
|
>>>>>>> b3c8fbd8 (chore: capture more events for analytics)
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course Review",
|
"name": "LMS Course Review",
|
||||||
|
|||||||
Reference in New Issue
Block a user