fix: notification on quiz update and mention
This commit is contained in:
@@ -2,18 +2,21 @@
|
|||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<div v-if="label" class="text-xs text-ink-gray-5 mb-2">
|
<div v-if="label" class="text-xs text-ink-gray-5 mb-2">
|
||||||
{{ __(label) }}
|
{{ __(label) }}
|
||||||
<span class="text-ink-red-3">*</span>
|
<span v-if="required" class="text-ink-red-3">*</span>
|
||||||
</div>
|
</div>
|
||||||
<FileUploader
|
<FileUploader
|
||||||
v-if="!modelValue"
|
v-if="!modelValue"
|
||||||
:fileTypes="['image/*']"
|
:fileTypes="[fileType]"
|
||||||
:validateFile="validateFile"
|
:validateFile="(file: File) => validateFile(file, true, type)"
|
||||||
@success="(file: File) => saveImage(file)"
|
@success="(file: File) => saveFile(file)"
|
||||||
>
|
>
|
||||||
<template v-slot="{ file, progress, uploading, openFileSelector }">
|
<template v-slot="{ file, progress, uploading, openFileSelector }">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="border rounded-md w-fit py-7 px-20">
|
<div class="border rounded-md w-fit py-7 px-20">
|
||||||
<Image class="size-5 stroke-1 text-ink-gray-7" />
|
<component
|
||||||
|
:is="props.type === 'image' ? Image : Video"
|
||||||
|
class="size-5 stroke-1 text-ink-gray-7"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<Button @click="openFileSelector">
|
<Button @click="openFileSelector">
|
||||||
@@ -28,7 +31,15 @@
|
|||||||
</FileUploader>
|
</FileUploader>
|
||||||
<div v-else class="mb-4">
|
<div v-else class="mb-4">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<img :src="modelValue" class="border rounded-md w-44 h-auto" />
|
<img
|
||||||
|
v-if="type == 'image'"
|
||||||
|
:src="modelValue"
|
||||||
|
class="border rounded-md w-44 h-auto"
|
||||||
|
/>
|
||||||
|
<video v-else controls class="border rounded-md w-44 h-auto">
|
||||||
|
<source :src="modelValue" />
|
||||||
|
{{ __('Your browser does not support the video tag.') }}
|
||||||
|
</video>
|
||||||
<div class="ml-4">
|
<div class="ml-4">
|
||||||
<Button @click="removeImage()">
|
<Button @click="removeImage()">
|
||||||
{{ __('Remove') }}
|
{{ __('Remove') }}
|
||||||
@@ -47,7 +58,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { validateFile } from '@/utils'
|
import { validateFile } from '@/utils'
|
||||||
import { Button, FileUploader } from 'frappe-ui'
|
import { Button, FileUploader } from 'frappe-ui'
|
||||||
import { Image } from 'lucide-vue-next'
|
import { Image, Video } from 'lucide-vue-next'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(e: 'update:modelValue', value: string): void
|
(e: 'update:modelValue', value: string): void
|
||||||
@@ -58,15 +70,23 @@ const props = withDefaults(
|
|||||||
modelValue: string
|
modelValue: string
|
||||||
label?: string
|
label?: string
|
||||||
description?: string
|
description?: string
|
||||||
|
type: 'image' | 'video'
|
||||||
|
required?: boolean
|
||||||
}>(),
|
}>(),
|
||||||
{
|
{
|
||||||
modelValue: '',
|
modelValue: '',
|
||||||
label: '',
|
label: '',
|
||||||
description: '',
|
description: '',
|
||||||
|
type: 'image',
|
||||||
|
required: true,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const saveImage = (file: any) => {
|
const fileType = computed(() => {
|
||||||
|
return props.type === 'image' ? 'image/*' : 'video/*'
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveFile = (file: any) => {
|
||||||
emit('update:modelValue', file.file_url)
|
emit('update:modelValue', file.file_url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-else-if class="text-ink-red-3">
|
<div v-else class="text-ink-red-3">
|
||||||
{{ __('No slots available for the selected course.') }}
|
{{ __('No slots available for the selected course.') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -202,60 +202,12 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<div>
|
<Uploader
|
||||||
<div class="text-xs text-ink-gray-5">
|
v-model="batch.video_link"
|
||||||
{{ __('Meta Image') }}
|
:label="__('Preview Video')"
|
||||||
</div>
|
type="video"
|
||||||
<FileUploader
|
:required="false"
|
||||||
v-if="!batch.image"
|
/>
|
||||||
:fileTypes="['image/*']"
|
|
||||||
:validateFile="validateFile"
|
|
||||||
@success="(file) => saveImage(file)"
|
|
||||||
>
|
|
||||||
<template
|
|
||||||
v-slot="{ file, progress, uploading, openFileSelector }"
|
|
||||||
>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div
|
|
||||||
class="border rounded-md w-fit py-5 px-5 md:px-20 cursor-pointer"
|
|
||||||
@click="openFileSelector"
|
|
||||||
>
|
|
||||||
<Image class="size-5 stroke-1 text-ink-gray-7" />
|
|
||||||
</div>
|
|
||||||
<div class="ml-4">
|
|
||||||
<Button @click="openFileSelector">
|
|
||||||
{{ __('Upload') }}
|
|
||||||
</Button>
|
|
||||||
<div class="mt-1 text-ink-gray-5 text-sm leading-5">
|
|
||||||
{{
|
|
||||||
__('Appears when the batch URL is shared on socials')
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</FileUploader>
|
|
||||||
<div v-else class="mb-4">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img
|
|
||||||
:src="batch.image.file_url"
|
|
||||||
class="border rounded-md w-40"
|
|
||||||
/>
|
|
||||||
<div class="ml-4">
|
|
||||||
<Button @click="removeImage()">
|
|
||||||
{{ __('Remove') }}
|
|
||||||
</Button>
|
|
||||||
<div class="mt-2 text-ink-gray-5 text-sm">
|
|
||||||
{{
|
|
||||||
__(
|
|
||||||
'Appears when the batch URL is shared on any online platform'
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -292,6 +244,12 @@
|
|||||||
{{ __('Meta Tags') }}
|
{{ __('Meta Tags') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
|
<Uploader
|
||||||
|
v-model="batch.meta_image"
|
||||||
|
:label="__('Meta Image')"
|
||||||
|
type="image"
|
||||||
|
:required="false"
|
||||||
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="meta.description"
|
v-model="meta.description"
|
||||||
:label="__('Meta Description')"
|
:label="__('Meta Description')"
|
||||||
@@ -345,8 +303,8 @@ import {
|
|||||||
openSettings,
|
openSettings,
|
||||||
sanitizeHTML,
|
sanitizeHTML,
|
||||||
updateMetaInfo,
|
updateMetaInfo,
|
||||||
validateFile,
|
|
||||||
} from '@/utils'
|
} from '@/utils'
|
||||||
|
import Uploader from '@/components/Controls/Uploader.vue'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
@@ -380,11 +338,12 @@ const batch = reactive({
|
|||||||
category: '',
|
category: '',
|
||||||
allow_self_enrollment: false,
|
allow_self_enrollment: false,
|
||||||
certification: false,
|
certification: false,
|
||||||
image: null,
|
meta_image: null,
|
||||||
paid_batch: false,
|
paid_batch: false,
|
||||||
currency: '',
|
currency: '',
|
||||||
amount: 0,
|
amount: 0,
|
||||||
zoom_account: '',
|
zoom_account: '',
|
||||||
|
video_link: '',
|
||||||
})
|
})
|
||||||
|
|
||||||
const meta = reactive({
|
const meta = reactive({
|
||||||
@@ -428,7 +387,8 @@ const newBatch = createResource({
|
|||||||
return {
|
return {
|
||||||
doc: {
|
doc: {
|
||||||
doctype: 'LMS Batch',
|
doctype: 'LMS Batch',
|
||||||
meta_image: batch.image?.file_url,
|
meta_image: batch.image,
|
||||||
|
video_link: batch.video_link,
|
||||||
instructors: instructors.value.map((instructor) => ({
|
instructors: instructors.value.map((instructor) => ({
|
||||||
instructor: instructor,
|
instructor: instructor,
|
||||||
})),
|
})),
|
||||||
@@ -447,39 +407,48 @@ const batchDetail = createResource({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
Object.keys(data).forEach((key) => {
|
updateBatchData(data)
|
||||||
if (key == 'instructors') {
|
|
||||||
data.instructors.forEach((instructor) => {
|
|
||||||
instructors.value.push(instructor.instructor)
|
|
||||||
})
|
|
||||||
} else if (['start_time', 'end_time'].includes(key)) {
|
|
||||||
let [hours, minutes, seconds] = data[key].split(':')
|
|
||||||
hours = hours.length == 1 ? '0' + hours : hours
|
|
||||||
batch[key] = `${hours}:${minutes}`
|
|
||||||
} else if (Object.hasOwn(batch, key)) batch[key] = data[key]
|
|
||||||
})
|
|
||||||
let checkboxes = [
|
|
||||||
'published',
|
|
||||||
'paid_batch',
|
|
||||||
'allow_self_enrollment',
|
|
||||||
'certification',
|
|
||||||
]
|
|
||||||
for (let idx in checkboxes) {
|
|
||||||
let key = checkboxes[idx]
|
|
||||||
batch[key] = batch[key] ? true : false
|
|
||||||
}
|
|
||||||
if (data.meta_image) imageResource.reload({ image: data.meta_image })
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const updateBatchData = (data) => {
|
||||||
|
Object.keys(data).forEach((key) => {
|
||||||
|
if (key == 'instructors') {
|
||||||
|
data.instructors.forEach((instructor) => {
|
||||||
|
instructors.value.push(instructor.instructor)
|
||||||
|
})
|
||||||
|
} else if (['start_time', 'end_time'].includes(key)) {
|
||||||
|
batch[key] = formatTime(data[key])
|
||||||
|
} else if (Object.hasOwn(batch, key)) batch[key] = data[key]
|
||||||
|
})
|
||||||
|
let checkboxes = [
|
||||||
|
'published',
|
||||||
|
'paid_batch',
|
||||||
|
'allow_self_enrollment',
|
||||||
|
'certification',
|
||||||
|
]
|
||||||
|
for (let idx in checkboxes) {
|
||||||
|
let key = checkboxes[idx]
|
||||||
|
batch[key] = batch[key] ? true : false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTime = (timeStr) => {
|
||||||
|
let [hours, minutes, seconds] = timeStr.split(':')
|
||||||
|
hours = hours.length == 1 ? '0' + hours : hours
|
||||||
|
return `${hours}:${minutes}`
|
||||||
|
}
|
||||||
|
|
||||||
const editBatch = createResource({
|
const editBatch = createResource({
|
||||||
url: 'frappe.client.set_value',
|
url: 'frappe.client.set_value',
|
||||||
makeParams(values) {
|
makeParams(values) {
|
||||||
|
console.log(batch.meta_image, batch.video_link)
|
||||||
return {
|
return {
|
||||||
doctype: 'LMS Batch',
|
doctype: 'LMS Batch',
|
||||||
name: props.batchName,
|
name: props.batchName,
|
||||||
fieldname: {
|
fieldname: {
|
||||||
meta_image: batch.image?.file_url,
|
meta_image: batch.meta_image,
|
||||||
|
video_link: batch.video_link,
|
||||||
instructors: instructors.value.map((instructor) => ({
|
instructors: instructors.value.map((instructor) => ({
|
||||||
instructor: instructor,
|
instructor: instructor,
|
||||||
})),
|
})),
|
||||||
@@ -489,19 +458,6 @@ const editBatch = createResource({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const imageResource = createResource({
|
|
||||||
url: 'lms.lms.api.get_file_info',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
file_url: values.image,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
auto: false,
|
|
||||||
onSuccess(data) {
|
|
||||||
batch.image = data
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const validateFields = () => {
|
const validateFields = () => {
|
||||||
batch.description = sanitizeHTML(batch.description)
|
batch.description = sanitizeHTML(batch.description)
|
||||||
batch.batch_details = sanitizeHTML(batch.batch_details)
|
batch.batch_details = sanitizeHTML(batch.batch_details)
|
||||||
@@ -603,14 +559,6 @@ const trashBatch = (close) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveImage = (file) => {
|
|
||||||
batch.image = file
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeImage = () => {
|
|
||||||
batch.image = null
|
|
||||||
}
|
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
const breadcrumbs = computed(() => {
|
||||||
let crumbs = [
|
let crumbs = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -23,14 +23,19 @@
|
|||||||
v-if="notifications?.length"
|
v-if="notifications?.length"
|
||||||
v-for="log in notifications"
|
v-for="log in notifications"
|
||||||
:key="log.name"
|
:key="log.name"
|
||||||
class="flex space-x-2 p-2 rounded-md"
|
class="flex space-x-2 px-2 py-4"
|
||||||
:class="{
|
:class="{
|
||||||
'cursor-pointer': log.link,
|
'cursor-pointer': log.link,
|
||||||
|
'items-center': !showDetails(log) && !isMention(log),
|
||||||
}"
|
}"
|
||||||
@click="navigateToPage(log)"
|
@click="navigateToPage(log)"
|
||||||
>
|
>
|
||||||
<Avatar :image="log.user_image" size="2xl" :label="log.full_name" />
|
<Avatar
|
||||||
<div class="space-y-2">
|
:image="log.from_user_details.user_image"
|
||||||
|
size="xl"
|
||||||
|
:label="log.from_user_details.full_name"
|
||||||
|
/>
|
||||||
|
<div class="space-y-2 w-full">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="text-ink-gray-9" v-html="log.subject"></div>
|
<div class="text-ink-gray-9" v-html="log.subject"></div>
|
||||||
@@ -42,7 +47,7 @@
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
v-if="!log.read"
|
v-if="!log.read"
|
||||||
@click.stop="(e) => handleMarkAsRead(e, log.name)"
|
@click.stop="(e) => handleMarkAsRead(log.name)"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<X class="h-4 w-4 text-ink-gray-7 stroke-1.5" />
|
<X class="h-4 w-4 text-ink-gray-7 stroke-1.5" />
|
||||||
@@ -51,19 +56,39 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="log.document_type == 'LMS Course' && log.document_details"
|
v-if="isMention(log)"
|
||||||
class="flex space-x-5 border border-outline-gray-2 p-2 rounded-md"
|
v-html="log.email_content"
|
||||||
|
class="bg-surface-gray-2 rounded-md px-3 py-2"
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
v-else-if="showDetails(log)"
|
||||||
|
class="flex items-stretch border border-outline-gray-2 space-x-2 rounded-md"
|
||||||
>
|
>
|
||||||
<iframe
|
<iframe
|
||||||
v-if="log.document_details.video_link"
|
v-if="
|
||||||
|
log.document_type == 'LMS Course' &&
|
||||||
|
log.document_details.video_link
|
||||||
|
"
|
||||||
:src="`https://www.youtube.com/embed/${log.document_details.video_link}`"
|
:src="`https://www.youtube.com/embed/${log.document_details.video_link}`"
|
||||||
class="rounded-md w-64"
|
class="rounded-l-md w-72"
|
||||||
/>
|
/>
|
||||||
<div class="">
|
<video
|
||||||
|
v-else-if="
|
||||||
|
log.document_type == 'LMS Batch' &&
|
||||||
|
log.document_details.video_link
|
||||||
|
"
|
||||||
|
:src="log.document_details.video_link"
|
||||||
|
class="rounded-l-md w-72"
|
||||||
|
/>
|
||||||
|
<div class="p-3">
|
||||||
<div
|
<div
|
||||||
class="bg-surface-violet-1 w-fit py-1 px-1.5 rounded-full text-ink-violet-1 text-sm mb-2"
|
class="bg-surface-violet-1 w-fit py-1 px-1.5 rounded-full text-ink-violet-1 text-sm mb-2"
|
||||||
>
|
>
|
||||||
{{ __('New Course') }}
|
{{
|
||||||
|
log.document_type === 'LMS Course'
|
||||||
|
? __('New Course')
|
||||||
|
: __('New Batch')
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
<div class="font-semibold mb-1">
|
<div class="font-semibold mb-1">
|
||||||
{{ __(log.document_details.title) }}
|
{{ __(log.document_details.title) }}
|
||||||
@@ -71,7 +96,31 @@
|
|||||||
<div class="leading-5">
|
<div class="leading-5">
|
||||||
{{ __(log.document_details.short_introduction) }}
|
{{ __(log.document_details.short_introduction) }}
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-5 space-y-2">
|
<div
|
||||||
|
v-if="log.document_details.start_date"
|
||||||
|
class="flex items-center space-x-2 text-sm mt-5"
|
||||||
|
>
|
||||||
|
<Calendar class="size-3 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{
|
||||||
|
dayjs(log.document_details.start_date).format('DD MMM YYYY')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="log.document_details.start_time"
|
||||||
|
class="flex items-center space-x-2 text-sm mt-2"
|
||||||
|
>
|
||||||
|
<Clock class="size-3 stroke-1.5" />
|
||||||
|
<span>
|
||||||
|
{{ formatTime(log.document_details.start_time) }}
|
||||||
|
{{ log.document_details.timezone }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="log.document_details.instructors.length > 1"
|
||||||
|
class="space-y-2 mt-5"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-for="instructor in log.document_details.instructors"
|
v-for="instructor in log.document_details.instructors"
|
||||||
class="flex items-center space-x-2"
|
class="flex items-center space-x-2"
|
||||||
@@ -81,7 +130,7 @@
|
|||||||
:image="instructor.user_image"
|
:image="instructor.user_image"
|
||||||
:label="instructor.full_name"
|
:label="instructor.full_name"
|
||||||
/>
|
/>
|
||||||
<span class="text-ink-gray-7 text-sm">
|
<span class="font-medium text-sm">
|
||||||
{{ instructor.full_name }}
|
{{ instructor.full_name }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -109,7 +158,8 @@ import {
|
|||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '../stores/session'
|
||||||
import { computed, inject, ref, onMounted, onUnmounted } from 'vue'
|
import { computed, inject, ref, onMounted, onUnmounted } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { X } from 'lucide-vue-next'
|
import { Calendar, Clock, X } from 'lucide-vue-next'
|
||||||
|
import { formatTime } from '@/utils/'
|
||||||
|
|
||||||
const { brand } = sessionStore()
|
const { brand } = sessionStore()
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
@@ -189,13 +239,37 @@ const navigateToPage = (log) => {
|
|||||||
params: { courseName: link[3] },
|
params: { courseName: link[3] },
|
||||||
})
|
})
|
||||||
} else if (link[2] == 'batches') {
|
} else if (link[2] == 'batches') {
|
||||||
router.push({
|
if (link[3] == 'details') {
|
||||||
name: 'Batch',
|
router.push({
|
||||||
params: { batchName: link[3] },
|
name: 'BatchDetail',
|
||||||
})
|
params: { batchName: link[4] },
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
router.push({
|
||||||
|
name: 'Batch',
|
||||||
|
params: { batchName: link[3] },
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMention = (log) => {
|
||||||
|
if (log.type == 'Mention') {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (log.subject.includes('mentioned you')) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const showDetails = (log) => {
|
||||||
|
return (
|
||||||
|
['LMS Course', 'LMS Batch'].includes(log.document_type) &&
|
||||||
|
log.document_details
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
socket.off('publish_lms_notifications')
|
socket.off('publish_lms_notifications')
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -618,15 +618,19 @@ export function singularize(word) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const validateFile = async (file, showToast = true) => {
|
export const validateFile = async (
|
||||||
|
file,
|
||||||
|
showToast = true,
|
||||||
|
fileType = 'image'
|
||||||
|
) => {
|
||||||
const error = (msg) => {
|
const error = (msg) => {
|
||||||
if (showToast) toast.error(msg)
|
if (showToast) toast.error(msg)
|
||||||
console.error(msg)
|
console.error(msg)
|
||||||
return msg
|
return msg
|
||||||
}
|
}
|
||||||
|
console.log(file.type, fileType)
|
||||||
if (!file.type.startsWith('image/')) {
|
if (!file.type.startsWith(`${fileType}/`)) {
|
||||||
return error(__('Only image file is allowed.'))
|
return error(__(`Only ${fileType} file is allowed.`))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.type === 'image/svg+xml') {
|
if (file.type === 'image/svg+xml') {
|
||||||
|
|||||||
+51
-6
@@ -1238,25 +1238,51 @@ def get_notifications(filters):
|
|||||||
notifications = frappe.get_all(
|
notifications = frappe.get_all(
|
||||||
"Notification Log",
|
"Notification Log",
|
||||||
filters,
|
filters,
|
||||||
["subject", "from_user", "link", "read", "name", "creation", "document_type", "document_name"],
|
[
|
||||||
|
"subject",
|
||||||
|
"from_user",
|
||||||
|
"link",
|
||||||
|
"read",
|
||||||
|
"name",
|
||||||
|
"creation",
|
||||||
|
"document_type",
|
||||||
|
"document_name",
|
||||||
|
"type",
|
||||||
|
"email_content",
|
||||||
|
],
|
||||||
order_by="creation desc",
|
order_by="creation desc",
|
||||||
)
|
)
|
||||||
|
|
||||||
for notification in notifications:
|
for notification in notifications:
|
||||||
notification = update_user_details(notification)
|
|
||||||
notification = update_document_details(notification)
|
notification = update_document_details(notification)
|
||||||
|
notification = update_user_details(notification)
|
||||||
|
|
||||||
return notifications
|
return notifications
|
||||||
|
|
||||||
|
|
||||||
def update_user_details(notification):
|
def update_user_details(notification):
|
||||||
from_user_details = frappe.db.get_value(
|
if (
|
||||||
"User", notification.from_user, ["full_name", "user_image"], as_dict=1
|
notification.document_details
|
||||||
)
|
and len(notification.document_details.get("instructors", []))
|
||||||
notification.update(from_user_details)
|
and not is_mention(notification)
|
||||||
|
):
|
||||||
|
from_user_details = notification.document_details["instructors"][0]
|
||||||
|
else:
|
||||||
|
from_user_details = frappe.db.get_value(
|
||||||
|
"User", notification.from_user, ["full_name", "user_image"], as_dict=1
|
||||||
|
)
|
||||||
|
notification["from_user_details"] = from_user_details
|
||||||
return notification
|
return notification
|
||||||
|
|
||||||
|
|
||||||
|
def is_mention(notification):
|
||||||
|
if notification.type == "Mention":
|
||||||
|
return True
|
||||||
|
if "mentioned you" in notification.subject.lower():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def update_document_details(notification):
|
def update_document_details(notification):
|
||||||
if notification.document_type == "LMS Course":
|
if notification.document_type == "LMS Course":
|
||||||
details = frappe.db.get_value(
|
details = frappe.db.get_value(
|
||||||
@@ -1265,6 +1291,25 @@ def update_document_details(notification):
|
|||||||
instructors = get_instructors("LMS Course", notification.document_name)
|
instructors = get_instructors("LMS Course", notification.document_name)
|
||||||
details["instructors"] = instructors
|
details["instructors"] = instructors
|
||||||
notification["document_details"] = details
|
notification["document_details"] = details
|
||||||
|
|
||||||
|
elif notification.document_type == "LMS Batch":
|
||||||
|
details = frappe.db.get_value(
|
||||||
|
"LMS Batch",
|
||||||
|
notification.document_name,
|
||||||
|
[
|
||||||
|
"title",
|
||||||
|
"description as short_introduction",
|
||||||
|
"video_link",
|
||||||
|
"start_date",
|
||||||
|
"end_date",
|
||||||
|
"start_time",
|
||||||
|
"timezone",
|
||||||
|
],
|
||||||
|
as_dict=1,
|
||||||
|
)
|
||||||
|
instructors = get_instructors("LMS Batch", notification.document_name)
|
||||||
|
details["instructors"] = instructors
|
||||||
|
notification["document_details"] = details
|
||||||
return notification
|
return notification
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
"description",
|
"description",
|
||||||
"column_break_hlqw",
|
"column_break_hlqw",
|
||||||
"instructors",
|
"instructors",
|
||||||
|
"video_link",
|
||||||
"zoom_account",
|
"zoom_account",
|
||||||
"section_break_rgfj",
|
"section_break_rgfj",
|
||||||
"medium",
|
"medium",
|
||||||
@@ -361,6 +362,11 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Zoom Account",
|
"label": "Zoom Account",
|
||||||
"options": "LMS Zoom Settings"
|
"options": "LMS Zoom Settings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "video_link",
|
||||||
|
"fieldtype": "Attach",
|
||||||
|
"label": "Preview Video"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
@@ -383,7 +389,7 @@
|
|||||||
"link_fieldname": "payment_for_document"
|
"link_fieldname": "payment_for_document"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-12-23 11:27:00.424331",
|
"modified": "2026-01-06 18:54:22.216656",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch",
|
"name": "LMS Batch",
|
||||||
|
|||||||
@@ -8,12 +8,14 @@ from datetime import timedelta
|
|||||||
import frappe
|
import frappe
|
||||||
import requests
|
import requests
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import add_days, cint, format_datetime, get_time, nowdate
|
from frappe.utils import add_days, cint, format_datetime, get_time, nowdate
|
||||||
|
|
||||||
from lms.lms.utils import (
|
from lms.lms.utils import (
|
||||||
generate_slug,
|
generate_slug,
|
||||||
get_assignment_details,
|
get_assignment_details,
|
||||||
|
get_instructors,
|
||||||
get_lesson_index,
|
get_lesson_index,
|
||||||
get_lesson_url,
|
get_lesson_url,
|
||||||
get_quiz_details,
|
get_quiz_details,
|
||||||
@@ -33,6 +35,10 @@ class LMSBatch(Document):
|
|||||||
self.validate_timetable()
|
self.validate_timetable()
|
||||||
self.validate_evaluation_end_date()
|
self.validate_evaluation_end_date()
|
||||||
|
|
||||||
|
def on_update(self):
|
||||||
|
if self.has_value_changed("published") and self.published:
|
||||||
|
frappe.enqueue(send_notification_for_published_batch, batch=self, now=True)
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
if not self.name:
|
if not self.name:
|
||||||
self.name = generate_slug(self.title, "LMS Batch")
|
self.name = generate_slug(self.title, "LMS Batch")
|
||||||
@@ -123,6 +129,80 @@ class LMSBatch(Document):
|
|||||||
update_payment_record("LMS Batch", self.name)
|
update_payment_record("LMS Batch", self.name)
|
||||||
|
|
||||||
|
|
||||||
|
def send_notification_for_published_batch(batch):
|
||||||
|
send_notification = frappe.db.get_single_value("LMS Settings", "send_notification_for_published_batches")
|
||||||
|
if not send_notification:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not batch.published:
|
||||||
|
return
|
||||||
|
|
||||||
|
if send_notification == "Email":
|
||||||
|
send_email_notification_for_published_batch(batch)
|
||||||
|
else:
|
||||||
|
send_system_notification_for_published_batch(batch)
|
||||||
|
|
||||||
|
|
||||||
|
def send_email_notification_for_published_batch(batch):
|
||||||
|
brand_name = frappe.db.get_single_value("Website Settings", "app_name")
|
||||||
|
brand_logo = frappe.db.get_single_value("Website Settings", "banner_image")
|
||||||
|
subject = _("A new course has been published on {0}").format(brand_name)
|
||||||
|
template = "published_batch_notification"
|
||||||
|
students = frappe.get_all("User", {"enabled": 1}, pluck="name")
|
||||||
|
instructors = get_instructors("LMS Batch", batch.name)
|
||||||
|
|
||||||
|
args = {
|
||||||
|
"brand_logo": brand_logo,
|
||||||
|
"brand_name": brand_name,
|
||||||
|
"title": batch.title,
|
||||||
|
"short_introduction": batch.description,
|
||||||
|
"start_date": batch.start_date,
|
||||||
|
"end_date": batch.end_date,
|
||||||
|
"start_time": batch.start_time,
|
||||||
|
"medium": batch.medium,
|
||||||
|
"timezone": batch.timezone,
|
||||||
|
"instructors": instructors,
|
||||||
|
"batch_url": f"{frappe.utils.get_url()}/lms/batches/details/{batch.name}",
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=instructors,
|
||||||
|
bcc=students,
|
||||||
|
subject=subject,
|
||||||
|
template=template,
|
||||||
|
args=args,
|
||||||
|
)
|
||||||
|
|
||||||
|
""" frappe.sendmail(
|
||||||
|
recipients=["jannat@frappe.io"],
|
||||||
|
subject=subject,
|
||||||
|
template=template,
|
||||||
|
args=args,
|
||||||
|
) """
|
||||||
|
|
||||||
|
|
||||||
|
def send_system_notification_for_published_batch(batch):
|
||||||
|
students = frappe.get_all("User", {"enabled": 1}, pluck="name")
|
||||||
|
instructors = frappe.get_all("Course Instructor", {"parent": batch.name}, pluck="instructor")
|
||||||
|
instructor_name = frappe.db.get_value("User", instructors[0], "full_name")
|
||||||
|
notification = frappe._dict(
|
||||||
|
{
|
||||||
|
"subject": _("{0} has published a new batch {1}").format(
|
||||||
|
frappe.bold(instructor_name), frappe.bold(batch.title)
|
||||||
|
),
|
||||||
|
"email_content": _(
|
||||||
|
"A new batch '{0}' has been published that might interest you. Check it out!"
|
||||||
|
).format(batch.title),
|
||||||
|
"document_type": "LMS Batch",
|
||||||
|
"document_name": batch.name,
|
||||||
|
"from_user": instructors[0] if instructors else None,
|
||||||
|
"type": "Alert",
|
||||||
|
"link": f"/lms/batches/details/{batch.name}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
make_notification_logs(notification, students)
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_live_class(
|
def create_live_class(
|
||||||
batch_name,
|
batch_name,
|
||||||
|
|||||||
@@ -134,10 +134,8 @@ class LMSCourse(Document):
|
|||||||
|
|
||||||
|
|
||||||
def send_notification_for_published_courses():
|
def send_notification_for_published_courses():
|
||||||
send_notification_for_published_courses = frappe.db.get_single_value(
|
send_notification = frappe.db.get_single_value("LMS Settings", "send_notification_for_published_courses")
|
||||||
"LMS Settings", "send_notification_for_published_courses"
|
if not send_notification:
|
||||||
)
|
|
||||||
if not send_notification_for_published_courses:
|
|
||||||
return
|
return
|
||||||
|
|
||||||
courses_published_today = frappe.get_all(
|
courses_published_today = frappe.get_all(
|
||||||
@@ -145,13 +143,13 @@ def send_notification_for_published_courses():
|
|||||||
{
|
{
|
||||||
"published_on": today(),
|
"published_on": today(),
|
||||||
},
|
},
|
||||||
["name", "title", "video_link", "short_introduction"],
|
["name", "title", "short_introduction"],
|
||||||
)
|
)
|
||||||
|
|
||||||
if not courses_published_today:
|
if not courses_published_today:
|
||||||
return
|
return
|
||||||
|
|
||||||
if send_notification_for_published_courses == "Email":
|
if send_notification == "Email":
|
||||||
send_email_notification_for_published_courses(courses_published_today)
|
send_email_notification_for_published_courses(courses_published_today)
|
||||||
else:
|
else:
|
||||||
send_system_notification_for_published_courses(courses_published_today)
|
send_system_notification_for_published_courses(courses_published_today)
|
||||||
@@ -170,7 +168,6 @@ def send_email_notification_for_published_courses(courses):
|
|||||||
args = {
|
args = {
|
||||||
"brand_logo": brand_logo,
|
"brand_logo": brand_logo,
|
||||||
"brand_name": brand_name,
|
"brand_name": brand_name,
|
||||||
"preview_video": f"https://www.youtube.com/embed/{course.video_link}",
|
|
||||||
"title": course.title,
|
"title": course.title,
|
||||||
"short_introduction": course.short_introduction,
|
"short_introduction": course.short_introduction,
|
||||||
"instructors": instructors,
|
"instructors": instructors,
|
||||||
|
|||||||
@@ -54,15 +54,15 @@ class LMSQuizSubmission(Document):
|
|||||||
notification = frappe._dict(
|
notification = frappe._dict(
|
||||||
{
|
{
|
||||||
"subject": _("You have got a score of {0} for the quiz {1}").format(
|
"subject": _("You have got a score of {0} for the quiz {1}").format(
|
||||||
self.score, self.quiz_title
|
(frappe.bold(self.score)), frappe.bold(self.quiz_title)
|
||||||
),
|
),
|
||||||
"email_content": _(
|
"email_content": _(
|
||||||
"There has been an update on your submission. You have got a score of {0} for the quiz {1}"
|
"There has been an update on your submission. You have got a score of {0} for the quiz {1}"
|
||||||
).format(self.score, self.quiz_title),
|
).format(frappe.bold(self.score), frappe.bold(self.quiz_title)),
|
||||||
"document_type": self.doctype,
|
"document_type": self.doctype,
|
||||||
"document_name": self.name,
|
"document_name": self.name,
|
||||||
"for_user": self.member,
|
"for_user": self.member,
|
||||||
"from_user": "Administrator",
|
"from_user": frappe.session.user,
|
||||||
"type": "Alert",
|
"type": "Alert",
|
||||||
"link": "",
|
"link": "",
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-4
@@ -444,12 +444,16 @@ def notify_mentions_on_portal(doc, topic):
|
|||||||
|
|
||||||
if topic.reference_doctype == "Course Lesson":
|
if topic.reference_doctype == "Course Lesson":
|
||||||
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
|
course = frappe.db.get_value("Course Lesson", topic.reference_docname, "course")
|
||||||
subject = _("{0} mentioned you in a comment in {1}").format(from_user_name, topic.title)
|
subject = _("{0} mentioned you in a comment in {1}").format(
|
||||||
|
frappe.bold(from_user_name), frappe.bold(topic.title)
|
||||||
|
)
|
||||||
link = get_lesson_url(course, get_lesson_index(topic.reference_docname))
|
link = get_lesson_url(course, get_lesson_index(topic.reference_docname))
|
||||||
else:
|
else:
|
||||||
batch_title = frappe.db.get_value("LMS Batch", topic.reference_docname, "title")
|
batch_title = frappe.db.get_value("LMS Batch", topic.reference_docname, "title")
|
||||||
subject = _("{0} mentioned you in a comment in {1}").format(from_user_name, batch_title)
|
subject = _("{0} mentioned you in a comment in {1}").format(
|
||||||
link = f"/batches/{topic.reference_docname}"
|
frappe.bold(from_user_name), frappe.bold(batch_title)
|
||||||
|
)
|
||||||
|
link = f"/lms/batches/{topic.reference_docname}"
|
||||||
|
|
||||||
for user in mentions:
|
for user in mentions:
|
||||||
notification = frappe._dict(
|
notification = frappe._dict(
|
||||||
@@ -460,7 +464,7 @@ def notify_mentions_on_portal(doc, topic):
|
|||||||
"document_name": topic.reference_docname,
|
"document_name": topic.reference_docname,
|
||||||
"for_user": user,
|
"for_user": user,
|
||||||
"from_user": doc.owner,
|
"from_user": doc.owner,
|
||||||
"type": "Alert",
|
"type": "Mention",
|
||||||
"link": link,
|
"link": link,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<div style="width: 70%; margin: 0 auto;">
|
||||||
|
<img src="{{ brand_logo }}" style="width: 30px; height: 30px;" />
|
||||||
|
<p style="font-size: 16px; font-weight: 600;">
|
||||||
|
{{ _("Hello Learner") }},
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
{{ _("A new batch has been published on ")}} {{ brand_name }} {{ _("that might interest you!") }} {{ _("Here are the details:") }}
|
||||||
|
</p>
|
||||||
|
<div style="background-color: #F8F8F8; border-radius: 12px; padding: 12px; margin-bottom: 6px;">
|
||||||
|
<div style="font-weight: 600; margin-bottom: 6px; font-size: 15px;">
|
||||||
|
{{ title }}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{{ short_introduction }}
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 20px; font-size: 13px;">
|
||||||
|
{% if end_date %}
|
||||||
|
<span>
|
||||||
|
{{ _("From ") }} {{ frappe.utils.format_date(start_date, "dd MMM YYYY") }} {{ _(" to ") }} {{ frappe.utils.format_date(end_date, "dd MMM YYYY") }}
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span>
|
||||||
|
{{ frappe.utils.format_date(start_date, "dd MMM YYYY") }}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div style="color: #525252; margin-top: 4px; font-size: 13px;">
|
||||||
|
<span>
|
||||||
|
{{ _("Time: ") }} {{ frappe.utils.format_time(start_time, "HH:mm a") }} {{ timezone }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
{% for instructor in instructors %}
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 5px;">
|
||||||
|
{% if instructor.user_image %}
|
||||||
|
<img src="{{ instructor.user_image }}" style="width: 20px; height: 20px; border-radius: 50%; margin-right: 5px;" />
|
||||||
|
{% else %}
|
||||||
|
<div style="width: 20px; height: 20px; border-radius: 50%; background-color: #ccc; display: flex; align-items: center; justify-content: center; margin-right: 5px;">
|
||||||
|
<span style="font-size: 12px; color: #fff;">
|
||||||
|
{{ instructor.full_name.split("")[0] | upper }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div>
|
||||||
|
{{ instructor.full_name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a href="{{ batch_url }}" style="display: inline-block; padding: 4px 8px; background-color: #171717; color: #fff; text-decoration: none; cursor: pointer; border-radius: 8px; margin-top: 10px;">
|
||||||
|
{{ _("Checkout the batch") }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user