Merge pull request #2108 from pateljannat/issues-185
fix: misc permission issues
This commit is contained in:
@@ -107,7 +107,7 @@
|
|||||||
<div class="flex flex-col gap-1 p-1">
|
<div class="flex flex-col gap-1 p-1">
|
||||||
<div class="text-base font-medium text-ink-gray-8">
|
<div class="text-base font-medium text-ink-gray-8">
|
||||||
{{
|
{{
|
||||||
option.value == option.label
|
option.value == option.label && option.description
|
||||||
? option.description
|
? option.description
|
||||||
: option.label
|
: option.label
|
||||||
}}
|
}}
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
v-if="groups.length == 0"
|
v-if="groups.length == 0"
|
||||||
class="mt-1.5 rounded-md px-2.5 py-1.5 text-base text-ink-gray-5"
|
class="mt-1.5 rounded-md px-2.5 py-1.5 text-base text-ink-gray-5"
|
||||||
>
|
>
|
||||||
No results found
|
{{ __('No results found') }}
|
||||||
</li>
|
</li>
|
||||||
</ComboboxOptions>
|
</ComboboxOptions>
|
||||||
<div v-if="slots.footer" class="border-t p-1.5 pb-0.5">
|
<div v-if="slots.footer" class="border-t p-1.5 pb-0.5">
|
||||||
|
|||||||
@@ -38,7 +38,7 @@
|
|||||||
'border object-cover',
|
'border object-cover',
|
||||||
shape === 'circle'
|
shape === 'circle'
|
||||||
? 'w-20 h-20 rounded-full'
|
? 'w-20 h-20 rounded-full'
|
||||||
: 'w-44 h-auto min-h-20 rounded-md',
|
: 'w-44 h-auto min-h-20 max-h-32 rounded-md',
|
||||||
]"
|
]"
|
||||||
/>
|
/>
|
||||||
<video v-else controls class="border rounded-md w-44 h-auto">
|
<video v-else controls class="border rounded-md w-44 h-auto">
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="grid gap-8 mt-10">
|
<div class="grid gap-8 mt-10">
|
||||||
<div v-for="(review, index) in reviews.data">
|
<div v-for="(review, index) in reviews.data">
|
||||||
<div class="flex items-center">
|
<div class="flex">
|
||||||
<router-link
|
<router-link
|
||||||
:to="{
|
:to="{
|
||||||
name: 'Profile',
|
name: 'Profile',
|
||||||
@@ -46,11 +46,11 @@
|
|||||||
"
|
"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="review.review" class="mt-4 leading-5 text-ink-gray-7">
|
||||||
|
{{ review.review }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="review.review" class="mt-4 leading-5 text-ink-gray-7">
|
|
||||||
{{ review.review }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
v-model="show"
|
v-model="show"
|
||||||
:options="{
|
:options="{
|
||||||
size: '4xl',
|
size: '4xl',
|
||||||
title: __('Video Statistics for {0}').format(lessonTitle),
|
title: __('Video Statistics'),
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
@@ -21,17 +21,22 @@
|
|||||||
class="mt-2 mr-5 w-[25%]"
|
class="mt-2 mr-5 w-[25%]"
|
||||||
/> -->
|
/> -->
|
||||||
</div>
|
</div>
|
||||||
<div v-if="currentTab" class="mt-4">
|
<div
|
||||||
|
v-if="currentTab"
|
||||||
|
:class="{
|
||||||
|
'mt-5': tabs.length > 1,
|
||||||
|
}"
|
||||||
|
>
|
||||||
<div class="grid grid-cols-[55%,40%] gap-5">
|
<div class="grid grid-cols-[55%,40%] gap-5">
|
||||||
<div
|
<div
|
||||||
class="space-y-5 border rounded-md p-2 pt-4 h-[70vh] overflow-y-auto"
|
class="space-y-5 border rounded-md p-2 pt-4 max-h-[70vh] overflow-y-auto"
|
||||||
>
|
>
|
||||||
<div class="grid grid-cols-[70%,30%] text-sm text-ink-gray-5">
|
<div class="grid grid-cols-[70%,30%] text-sm text-ink-gray-5">
|
||||||
<div class="px-4">
|
<div class="px-4">
|
||||||
{{ __('Member') }}
|
{{ __('Member') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
{{ __('Watch Time') }}
|
{{ __('Watch Time (mins)') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
@@ -68,15 +73,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-5">
|
<div class="space-y-5">
|
||||||
<NumberChart
|
<NumberChartGraph
|
||||||
class="border rounded-md"
|
:title="__('Average Watch Time (mins)')"
|
||||||
:config="{
|
:value="averageWatchTime"
|
||||||
title: __('Average Watch Time'),
|
|
||||||
value: averageWatchTime,
|
|
||||||
}"
|
|
||||||
/>
|
/>
|
||||||
<div v-if="isPlyrSource">
|
<div v-if="isPlyrSource">
|
||||||
<div class="video-player" :src="currentTab"></div>
|
<div
|
||||||
|
class="video-player"
|
||||||
|
:data-plyr-provider="provider"
|
||||||
|
:src="currentTab"
|
||||||
|
></div>
|
||||||
</div>
|
</div>
|
||||||
<VideoBlock v-else :file="currentTab" />
|
<VideoBlock v-else :file="currentTab" />
|
||||||
</div>
|
</div>
|
||||||
@@ -101,6 +107,7 @@ import {
|
|||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { enablePlyr, formatTimestamp } from '@/utils'
|
import { enablePlyr, formatTimestamp } from '@/utils'
|
||||||
import VideoBlock from '@/components/VideoBlock.vue'
|
import VideoBlock from '@/components/VideoBlock.vue'
|
||||||
|
import NumberChartGraph from '@/components/NumberChartGraph.vue'
|
||||||
|
|
||||||
const show = defineModel<boolean | undefined>()
|
const show = defineModel<boolean | undefined>()
|
||||||
const currentTab = ref<string>('')
|
const currentTab = ref<string>('')
|
||||||
@@ -171,7 +178,7 @@ watch(show, () => {
|
|||||||
|
|
||||||
const statisticsData = computed(() => {
|
const statisticsData = computed(() => {
|
||||||
const grouped = <Record<string, any[]>>{}
|
const grouped = <Record<string, any[]>>{}
|
||||||
statistics.data.forEach((item: { source: string }) => {
|
statistics.data?.forEach((item: { source: string }) => {
|
||||||
if (!grouped[item.source]) {
|
if (!grouped[item.source]) {
|
||||||
grouped[item.source] = []
|
grouped[item.source] = []
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ const referenceDoctypeOptions = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const eventOptions = computed(() => {
|
const eventOptions = computed(() => {
|
||||||
let options = ['New', 'Value Change', 'Auto Assign']
|
let options = ['New', 'Value Change', 'Manual Assignment']
|
||||||
return options.map((event) => ({ label: __(event), value: event }))
|
return options.map((event) => ({ label: __(event), value: event }))
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -6,16 +6,18 @@
|
|||||||
<div class="text-xl font-semibold leading-none text-ink-gray-9">
|
<div class="text-xl font-semibold leading-none text-ink-gray-9">
|
||||||
{{ __(label) }}
|
{{ __(label) }}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="space-x-2">
|
||||||
<Badge
|
<Badge
|
||||||
v-if="data.isDirty"
|
v-if="data.isDirty"
|
||||||
:label="__('Not Saved')"
|
:label="__('Not Saved')"
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
theme="orange"
|
theme="orange"
|
||||||
/>
|
/>
|
||||||
|
<Button variant="solid" :loading="data.save.loading" @click="update">
|
||||||
|
{{ __('Update') }}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="solid" :loading="data.save.loading" @click="update">
|
|
||||||
{{ __('Update') }}
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="text-ink-gray-6 leading-5">
|
<div class="text-ink-gray-6 leading-5">
|
||||||
{{ __(description) }}
|
{{ __(description) }}
|
||||||
|
|||||||
@@ -219,6 +219,25 @@ const tabsStructure = computed(() => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Jobs',
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
label: 'Allow Job Posting',
|
||||||
|
name: 'allow_job_posting',
|
||||||
|
type: 'checkbox',
|
||||||
|
description:
|
||||||
|
'If enabled, users can post job openings on the job board. Else only admins can post jobs.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fields: [],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: '',
|
label: '',
|
||||||
columns: [
|
columns: [
|
||||||
|
|||||||
@@ -31,12 +31,14 @@
|
|||||||
<div v-if="upcoming_evals.data?.length">
|
<div v-if="upcoming_evals.data?.length">
|
||||||
<div
|
<div
|
||||||
class="grid gap-4"
|
class="grid gap-4"
|
||||||
:class="forHome ? 'grid-cols-1 md:grid-cols-2' : 'grid-cols-1'"
|
:class="forHome ? 'grid-cols-1 md:grid-cols-4' : 'grid-cols-1'"
|
||||||
>
|
>
|
||||||
<div v-for="evl in upcoming_evals.data">
|
<div v-for="evl in upcoming_evals.data">
|
||||||
<div class="border text-ink-gray-7 rounded-md p-3">
|
<div
|
||||||
|
class="border hover:border-outline-gray-3 text-ink-gray-7 rounded-md p-3"
|
||||||
|
>
|
||||||
<div class="flex justify-between mb-3">
|
<div class="flex justify-between mb-3">
|
||||||
<span class="text-lg font-semibold text-ink-gray-9 leading-5">
|
<span class="font-semibold text-ink-gray-9 leading-5">
|
||||||
{{ evl.course_title }}
|
{{ evl.course_title }}
|
||||||
</span>
|
</span>
|
||||||
<Menu
|
<Menu
|
||||||
|
|||||||
@@ -1,82 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div v-if="badge.data">
|
|
||||||
<div class="p-5 flex flex-col items-center mt-40">
|
|
||||||
<div class="text-3xl font-semibold">
|
|
||||||
{{ badge.data.badge }}
|
|
||||||
</div>
|
|
||||||
<img
|
|
||||||
:src="badge.data.badge_image"
|
|
||||||
:alt="badge.data.badge"
|
|
||||||
class="h-60 mt-2"
|
|
||||||
/>
|
|
||||||
<div class="">
|
|
||||||
{{
|
|
||||||
__('This badge has been awarded to {0} on {1}.').format(
|
|
||||||
badge.data.member_name,
|
|
||||||
dayjs(badge.data.issued_on).format('DD MMM YYYY')
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
{{ badge.data.badge_description }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<script setup>
|
|
||||||
import { createResource, usePageMeta } from 'frappe-ui'
|
|
||||||
import { computed, inject } from 'vue'
|
|
||||||
import { sessionStore } from '../stores/session'
|
|
||||||
|
|
||||||
const dayjs = inject('$dayjs')
|
|
||||||
const { brand } = sessionStore()
|
|
||||||
|
|
||||||
const props = defineProps({
|
|
||||||
badgeName: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
email: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const badge = createResource({
|
|
||||||
url: 'frappe.client.get',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doctype: 'LMS Badge Assignment',
|
|
||||||
filters: {
|
|
||||||
badge: props.badgeName,
|
|
||||||
member: props.email,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
auto: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
const breadcrumbs = computed(() => {
|
|
||||||
return [
|
|
||||||
{
|
|
||||||
label: __('Badges'),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: badge.data.badge,
|
|
||||||
route: {
|
|
||||||
name: 'Badge',
|
|
||||||
params: {
|
|
||||||
badge: badge.data.badge,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
|
||||||
})
|
|
||||||
|
|
||||||
usePageMeta(() => {
|
|
||||||
return {
|
|
||||||
title: badge.data.badge,
|
|
||||||
icon: brand.favicon,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
}
|
}
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<div class="font-semibold text-ink-gray-9 text-lg mb-1">
|
<div class="font-semibold text-ink-gray-9 mb-1">
|
||||||
{{ cls.title }}
|
{{ cls.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="short-introduction">
|
<div class="short-introduction">
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-5 mt-10">
|
<div class="mt-10 space-y-10">
|
||||||
<UpcomingEvaluations :forHome="true" />
|
<UpcomingEvaluations :forHome="true" />
|
||||||
<div v-if="myLiveClasses.data?.length">
|
<div v-if="myLiveClasses.data?.length">
|
||||||
<div class="font-semibold text-lg mb-3 text-ink-gray-9">
|
<div class="font-semibold text-lg mb-3 text-ink-gray-9">
|
||||||
{{ __('Upcoming Live Classes') }}
|
{{ __('Upcoming Live Classes') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
|
<div class="grid grid-cols-1 md:grid-cols-4 gap-5">
|
||||||
<div
|
<div
|
||||||
v-for="cls in myLiveClasses.data"
|
v-for="cls in myLiveClasses.data"
|
||||||
class="border rounded-md hover:border-outline-gray-3 p-2"
|
class="border rounded-md hover:border-outline-gray-3 p-3"
|
||||||
>
|
>
|
||||||
<div class="font-semibold text-ink-gray-9 text-lg leading-5 mb-1">
|
<div class="font-semibold text-ink-gray-9 leading-5 mb-1">
|
||||||
{{ cls.title }}
|
{{ cls.title }}
|
||||||
</div>
|
</div>
|
||||||
<div class="text-ink-gray-5 leading-5 mb-4">
|
<div class="text-ink-gray-5 leading-5 mb-4">
|
||||||
|
|||||||
+127
-107
@@ -4,9 +4,14 @@
|
|||||||
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
class="sticky top-0 z-10 flex items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||||
>
|
>
|
||||||
<Breadcrumbs :items="breadcrumbs" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
<Button variant="solid" @click="saveJob()">
|
<div class="space-x-2">
|
||||||
{{ __('Save') }}
|
<Badge v-if="isDirty" theme="orange">
|
||||||
</Button>
|
{{ __('Not Saved') }}
|
||||||
|
</Badge>
|
||||||
|
<Button variant="solid" @click="saveJob()">
|
||||||
|
{{ __('Save') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<div class="py-5">
|
<div class="py-5">
|
||||||
<div class="container border-b mb-4 pb-5">
|
<div class="container border-b mb-4 pb-5">
|
||||||
@@ -109,15 +114,25 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
|
Badge,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
|
call,
|
||||||
FormControl,
|
FormControl,
|
||||||
createResource,
|
createDocumentResource,
|
||||||
Button,
|
Button,
|
||||||
TextEditor,
|
TextEditor,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
toast,
|
toast,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { computed, onMounted, reactive, inject } from 'vue'
|
import {
|
||||||
|
computed,
|
||||||
|
inject,
|
||||||
|
onMounted,
|
||||||
|
onBeforeUnmount,
|
||||||
|
reactive,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from 'vue'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { escapeHTML, sanitizeHTML } from '@/utils'
|
import { escapeHTML, sanitizeHTML } from '@/utils'
|
||||||
@@ -126,6 +141,8 @@ import Uploader from '@/components/Controls/Uploader.vue'
|
|||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const { brand } = sessionStore()
|
const { brand } = sessionStore()
|
||||||
|
const isDirty = ref(false)
|
||||||
|
const originalJobData = ref(null)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
jobName: {
|
jobName: {
|
||||||
@@ -134,67 +151,6 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const newJob = createResource({
|
|
||||||
url: 'frappe.client.insert',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doc: {
|
|
||||||
doctype: 'Job Opportunity',
|
|
||||||
company_logo: job.company_logo,
|
|
||||||
...job,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const updateJob = createResource({
|
|
||||||
url: 'frappe.client.set_value',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doctype: 'Job Opportunity',
|
|
||||||
name: props.jobName,
|
|
||||||
fieldname: {
|
|
||||||
company_logo: job.company_logo,
|
|
||||||
...job,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const jobDetail = createResource({
|
|
||||||
url: 'frappe.client.get',
|
|
||||||
makeParams(values) {
|
|
||||||
return {
|
|
||||||
doctype: 'Job Opportunity',
|
|
||||||
name: props.jobName,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onSuccess(data) {
|
|
||||||
if (data.owner != user.data?.name && !user.data?.is_moderator) {
|
|
||||||
router.push({
|
|
||||||
name: 'Jobs',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
Object.keys(data).forEach((key) => {
|
|
||||||
if (Object.hasOwn(job, key)) job[key] = data[key]
|
|
||||||
})
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
const job = reactive({
|
|
||||||
job_title: '',
|
|
||||||
location: '',
|
|
||||||
country: '',
|
|
||||||
type: 'Full Time',
|
|
||||||
work_mode: 'On-site',
|
|
||||||
status: 'Open',
|
|
||||||
company_name: '',
|
|
||||||
company_website: '',
|
|
||||||
company_logo: null,
|
|
||||||
description: '',
|
|
||||||
company_email_address: '',
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!user.data) {
|
if (!user.data) {
|
||||||
router.push({
|
router.push({
|
||||||
@@ -202,22 +158,64 @@ onMounted(() => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.jobName != 'new') jobDetail.reload()
|
if (props.jobName != 'new') jobDetails.reload()
|
||||||
addKeyboardShortcuts()
|
window.addEventListener('keydown', keyboardShortcut)
|
||||||
})
|
})
|
||||||
|
|
||||||
const addKeyboardShortcuts = () => {
|
const job = reactive({
|
||||||
document.addEventListener('keydown', (e) => {
|
job_title: '',
|
||||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 's') {
|
type: '',
|
||||||
e.preventDefault()
|
work_mode: '',
|
||||||
saveJob()
|
location: '',
|
||||||
|
country: '',
|
||||||
|
status: 'Open',
|
||||||
|
description: '',
|
||||||
|
company_name: '',
|
||||||
|
company_website: '',
|
||||||
|
company_email_address: '',
|
||||||
|
company_logo: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
const jobDetails = createDocumentResource({
|
||||||
|
doctype: 'Job Opportunity',
|
||||||
|
name: props.jobName != 'new' ? props.jobName : undefined,
|
||||||
|
onError(err) {
|
||||||
|
toast.error(err.messages?.[0] || err)
|
||||||
|
console.error(err)
|
||||||
|
},
|
||||||
|
auto: props.jobName != 'new',
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => jobDetails?.doc,
|
||||||
|
() => {
|
||||||
|
if (!jobDetails.doc) return
|
||||||
|
if (jobDetails.doc.owner != user.data?.name && !user.data?.is_moderator) {
|
||||||
|
router.push({
|
||||||
|
name: 'Jobs',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
})
|
|
||||||
}
|
if (jobDetails.doc) {
|
||||||
|
Object.assign(job, jobDetails.doc)
|
||||||
|
originalJobData.value = JSON.parse(JSON.stringify(jobDetails.doc))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
job,
|
||||||
|
() => {
|
||||||
|
isDirty.value = Object.keys(job).some((key) => {
|
||||||
|
return job[key] != originalJobData.value?.[key]
|
||||||
|
})
|
||||||
|
},
|
||||||
|
{ deep: true }
|
||||||
|
)
|
||||||
|
|
||||||
const saveJob = () => {
|
const saveJob = () => {
|
||||||
validateJobFields()
|
validateJobFields()
|
||||||
if (jobDetail.data) {
|
if (jobDetails?.doc) {
|
||||||
editJobDetails()
|
editJobDetails()
|
||||||
} else {
|
} else {
|
||||||
createNewJob()
|
createNewJob()
|
||||||
@@ -225,38 +223,46 @@ const saveJob = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const createNewJob = () => {
|
const createNewJob = () => {
|
||||||
newJob.submit(
|
call('frappe.client.insert', {
|
||||||
{},
|
doc: {
|
||||||
{
|
doctype: 'Job Opportunity',
|
||||||
onSuccess(data) {
|
company_logo: job.company_logo,
|
||||||
router.push({
|
...job,
|
||||||
name: 'JobDetail',
|
},
|
||||||
params: {
|
})
|
||||||
job: data.name,
|
.then((data) => {
|
||||||
},
|
router.push({
|
||||||
})
|
name: 'JobDetail',
|
||||||
},
|
params: {
|
||||||
onError(err) {
|
job: data.name,
|
||||||
toast.error(err.messages?.[0] || err)
|
},
|
||||||
},
|
})
|
||||||
}
|
})
|
||||||
)
|
.catch((err) => {
|
||||||
|
toast.error(err.messages?.[0] || err)
|
||||||
|
console.error(err)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const editJobDetails = () => {
|
const editJobDetails = () => {
|
||||||
updateJob.submit(
|
jobDetails.setValue.submit(
|
||||||
{},
|
{
|
||||||
|
company_logo: job.company_logo,
|
||||||
|
...job,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
|
jobDetails.reload()
|
||||||
router.push({
|
router.push({
|
||||||
name: 'JobDetail',
|
name: 'JobDetail',
|
||||||
params: {
|
params: {
|
||||||
job: data.name,
|
job: props.jobName,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
toast.error(err.messages?.[0] || err)
|
toast.error(err.messages?.[0] || err)
|
||||||
|
console.error(err)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -271,27 +277,38 @@ const validateJobFields = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const keyboardShortcut = (e) => {
|
||||||
|
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 's') {
|
||||||
|
e.preventDefault()
|
||||||
|
saveJob()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
window.removeEventListener('keydown', keyboardShortcut)
|
||||||
|
})
|
||||||
|
|
||||||
const jobTypes = computed(() => {
|
const jobTypes = computed(() => {
|
||||||
return [
|
return [
|
||||||
{ label: 'Full Time', value: 'Full Time' },
|
{ label: __('Full Time'), value: 'Full Time' },
|
||||||
{ label: 'Part Time', value: 'Part Time' },
|
{ label: __('Part Time'), value: 'Part Time' },
|
||||||
{ label: 'Contract', value: 'Contract' },
|
{ label: __('Contract'), value: 'Contract' },
|
||||||
{ label: 'Freelance', value: 'Freelance' },
|
{ label: __('Freelance'), value: 'Freelance' },
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const workModes = computed(() => {
|
const workModes = computed(() => {
|
||||||
return [
|
return [
|
||||||
{ label: 'On site', value: 'On-site' },
|
{ label: __('On site'), value: 'On-site' },
|
||||||
{ label: 'Hybrid', value: 'Hybrid' },
|
{ label: __('Hybrid'), value: 'Hybrid' },
|
||||||
{ label: 'Remote', value: 'Remote' },
|
{ label: __('Remote'), value: 'Remote' },
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
const jobStatuses = computed(() => {
|
const jobStatuses = computed(() => {
|
||||||
return [
|
return [
|
||||||
{ label: 'Open', value: 'Open' },
|
{ label: __('Open'), value: 'Open' },
|
||||||
{ label: 'Closed', value: 'Closed' },
|
{ label: __('Closed'), value: 'Closed' },
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -302,8 +319,11 @@ const breadcrumbs = computed(() => {
|
|||||||
route: { name: 'Jobs' },
|
route: { name: 'Jobs' },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: props.jobName == 'new' ? __('New Job') : __('Edit Job'),
|
label: props.jobName == 'new' ? __('New Job') : jobDetails.doc?.job_title,
|
||||||
route: { name: 'JobForm' },
|
route:
|
||||||
|
props.jobName == 'new'
|
||||||
|
? {}
|
||||||
|
: { name: 'JobDetail', params: { job: props.jobName } },
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
return crumbs
|
return crumbs
|
||||||
@@ -311,7 +331,7 @@ const breadcrumbs = computed(() => {
|
|||||||
|
|
||||||
usePageMeta(() => {
|
usePageMeta(() => {
|
||||||
return {
|
return {
|
||||||
title: props.jobName == 'new' ? __('New Job') : jobDetail.data?.job_title,
|
title: props.jobName == 'new' ? __('New Job') : jobDetails.doc?.job_title,
|
||||||
icon: brand.favicon,
|
icon: brand.favicon,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
:items="[{ label: __('Jobs'), route: { name: 'Jobs' } }]"
|
:items="[{ label: __('Jobs'), route: { name: 'Jobs' } }]"
|
||||||
/>
|
/>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="user.data?.name"
|
v-if="
|
||||||
|
user.data?.name && settings.data?.allow_job_posting && !readOnlyMode
|
||||||
|
"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'JobForm',
|
name: 'JobForm',
|
||||||
params: {
|
params: {
|
||||||
@@ -16,7 +18,7 @@
|
|||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<Button v-if="!readOnlyMode" variant="solid">
|
<Button variant="solid">
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<Plus class="h-4 w-4" />
|
<Plus class="h-4 w-4" />
|
||||||
</template>
|
</template>
|
||||||
@@ -123,7 +125,8 @@ import {
|
|||||||
usePageMeta,
|
usePageMeta,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { Plus, Search } from 'lucide-vue-next'
|
import { Plus, Search } from 'lucide-vue-next'
|
||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
|
import { useSettings } from '@/stores/settings'
|
||||||
import { inject, computed, ref, onMounted, watch } from 'vue'
|
import { inject, computed, ref, onMounted, watch } from 'vue'
|
||||||
import JobCard from '@/components/JobCard.vue'
|
import JobCard from '@/components/JobCard.vue'
|
||||||
import Link from '@/components/Controls/Link.vue'
|
import Link from '@/components/Controls/Link.vue'
|
||||||
@@ -133,6 +136,7 @@ const user = inject('$user')
|
|||||||
const jobType = ref(null)
|
const jobType = ref(null)
|
||||||
const workMode = ref(null)
|
const workMode = ref(null)
|
||||||
const { brand } = sessionStore()
|
const { brand } = sessionStore()
|
||||||
|
const { settings } = useSettings()
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
const country = ref(null)
|
const country = ref(null)
|
||||||
const filters = ref({})
|
const filters = ref({})
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Button v-if="canSeeStats()" @click="showVideoStats()">
|
<Button v-if="isAdmin" @click="showVideoStats()">
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<TrendingUp class="size-4 stroke-1.5" />
|
<TrendingUp class="size-4 stroke-1.5" />
|
||||||
</template>
|
</template>
|
||||||
@@ -326,7 +326,7 @@
|
|||||||
@updateNotes="updateNotes"
|
@updateNotes="updateNotes"
|
||||||
/>
|
/>
|
||||||
<VideoStatistics
|
<VideoStatistics
|
||||||
v-if="showStatsDialog"
|
v-if="isAdmin"
|
||||||
v-model="showStatsDialog"
|
v-model="showStatsDialog"
|
||||||
:lessonName="lesson.data?.name"
|
:lessonName="lesson.data?.name"
|
||||||
:lessonTitle="lesson.data?.title"
|
:lessonTitle="lesson.data?.title"
|
||||||
@@ -524,7 +524,14 @@ const renderEditor = (holder, content) => {
|
|||||||
|
|
||||||
const markProgress = () => {
|
const markProgress = () => {
|
||||||
if (user.data && lesson.data && !lesson.data.progress) {
|
if (user.data && lesson.data && !lesson.data.progress) {
|
||||||
progress.submit()
|
progress.submit(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
onError(err) {
|
||||||
|
console.error(err)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -605,7 +612,6 @@ watch(
|
|||||||
plyrSources.value = []
|
plyrSources.value = []
|
||||||
await nextTick()
|
await nextTick()
|
||||||
resetLessonState(newChapterNumber, newLessonNumber)
|
resetLessonState(newChapterNumber, newLessonNumber)
|
||||||
startTimer()
|
|
||||||
updateNotes()
|
updateNotes()
|
||||||
checkIfDiscussionsAllowed()
|
checkIfDiscussionsAllowed()
|
||||||
checkQuiz()
|
checkQuiz()
|
||||||
@@ -674,6 +680,7 @@ watch(
|
|||||||
() => lesson.data,
|
() => lesson.data,
|
||||||
async (data) => {
|
async (data) => {
|
||||||
setupLesson(data)
|
setupLesson(data)
|
||||||
|
startTimer()
|
||||||
getPlyrSource()
|
getPlyrSource()
|
||||||
updateNotes()
|
updateNotes()
|
||||||
if (data.icon == 'icon-youtube') clearInterval(timerInterval)
|
if (data.icon == 'icon-youtube') clearInterval(timerInterval)
|
||||||
@@ -769,17 +776,19 @@ const checkIfDiscussionsAllowed = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isAdmin = computed(() => {
|
||||||
|
let isInstructor = lesson.data?.instructors?.includes(user.data?.name)
|
||||||
|
return user.data?.is_moderator || isInstructor
|
||||||
|
})
|
||||||
|
|
||||||
const allowEdit = () => {
|
const allowEdit = () => {
|
||||||
if (window.read_only_mode) return false
|
if (window.read_only_mode) return false
|
||||||
if (user.data?.is_moderator) return true
|
if (isAdmin.value) return true
|
||||||
if (lesson.data?.instructors?.includes(user.data?.name)) return true
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowInstructorContent = () => {
|
const allowInstructorContent = () => {
|
||||||
if (user.data?.is_moderator) return true
|
return isAdmin.value
|
||||||
if (lesson.data?.instructors?.includes(user.data?.name)) return true
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const enrollment = createResource({
|
const enrollment = createResource({
|
||||||
@@ -819,11 +828,6 @@ const toggleInlineMenu = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const canSeeStats = () => {
|
|
||||||
if (user.data?.is_moderator || user.data?.is_instructor) return true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const showVideoStats = () => {
|
const showVideoStats = () => {
|
||||||
showStatsDialog.value = true
|
showStatsDialog.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -70,13 +70,16 @@
|
|||||||
<div class="leading-5 mb-4">
|
<div class="leading-5 mb-4">
|
||||||
{{ badge.badge_description }}
|
{{ badge.badge_description }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col mb-4">
|
<div class="flex flex-col">
|
||||||
<span class="text-xs text-ink-gray-7 font-medium mb-1">
|
<span class="text-xs text-ink-gray-7 font-medium mb-1">
|
||||||
{{ __('Issued on') }}:
|
{{ __('Issued on') }}:
|
||||||
</span>
|
</span>
|
||||||
{{ dayjs(badge.issued_on).format('DD MMM YYYY') }}
|
{{ dayjs(badge.issued_on).format('DD MMM YYYY') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col">
|
<div
|
||||||
|
v-if="user.data?.name == profile.data?.name"
|
||||||
|
class="flex flex-col mt-4"
|
||||||
|
>
|
||||||
<span class="text-xs text-ink-gray-7 font-medium mb-1">
|
<span class="text-xs text-ink-gray-7 font-medium mb-1">
|
||||||
{{ __('Share on') }}:
|
{{ __('Share on') }}:
|
||||||
</span>
|
</span>
|
||||||
@@ -125,6 +128,7 @@ import DOMPurify from 'dompurify'
|
|||||||
import { getLmsRoute } from '@/utils/basePath'
|
import { getLmsRoute } from '@/utils/basePath'
|
||||||
|
|
||||||
const dayjs = inject('$dayjs')
|
const dayjs = inject('$dayjs')
|
||||||
|
const user = inject('$user')
|
||||||
const { branding } = sessionStore()
|
const { branding } = sessionStore()
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -135,13 +139,9 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const badges = createResource({
|
const badges = createResource({
|
||||||
url: 'frappe.client.get_list',
|
url: 'lms.lms.api.get_badges',
|
||||||
params: {
|
params: {
|
||||||
doctype: 'LMS Badge Assignment',
|
member: props.profile.data.name,
|
||||||
fields: ['name', 'badge', 'badge_image', 'badge_description', 'issued_on'],
|
|
||||||
filters: {
|
|
||||||
member: props.profile.data.name,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
auto: true,
|
auto: true,
|
||||||
transform(data) {
|
transform(data) {
|
||||||
@@ -160,14 +160,16 @@ const shareOnSocial = (badge, medium) => {
|
|||||||
let shareUrl
|
let shareUrl
|
||||||
const url = encodeURIComponent(
|
const url = encodeURIComponent(
|
||||||
`${window.location.origin}${getLmsRoute(
|
`${window.location.origin}${getLmsRoute(
|
||||||
`badges/${badge.badge}/${props.profile.data?.email}`
|
`user/${props.profile.data?.username}`
|
||||||
)}`
|
)}`
|
||||||
)
|
)
|
||||||
const summary = `I am happy to announce that I earned the ${
|
const summary = __(
|
||||||
badge.badge
|
'I am happy to announce that I earned the {0} badge on {1} at {2}'
|
||||||
} badge on ${dayjs(badge.issued_on).format('DD MMM YYYY')} at ${
|
).format(
|
||||||
|
badge.badge,
|
||||||
|
dayjs(badge.issued_on).format('DD MMM YYYY'),
|
||||||
branding.data?.app_name
|
branding.data?.app_name
|
||||||
}.`
|
)
|
||||||
|
|
||||||
if (medium == 'LinkedIn')
|
if (medium == 'LinkedIn')
|
||||||
shareUrl = `https://www.linkedin.com/shareArticle?mini=true&url=${url}&text=${summary}`
|
shareUrl = `https://www.linkedin.com/shareArticle?mini=true&url=${url}&text=${summary}`
|
||||||
|
|||||||
@@ -139,12 +139,6 @@ const routes = [
|
|||||||
name: 'Notifications',
|
name: 'Notifications',
|
||||||
component: () => import('@/pages/Notifications.vue'),
|
component: () => import('@/pages/Notifications.vue'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/badges/:badgeName/:email',
|
|
||||||
name: 'Badge',
|
|
||||||
component: () => import('@/pages/Badge.vue'),
|
|
||||||
props: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/quizzes',
|
path: '/quizzes',
|
||||||
name: 'Quizzes',
|
name: 'Quizzes',
|
||||||
|
|||||||
+10
-7
@@ -86,13 +86,16 @@ after_migrate = [
|
|||||||
# -----------
|
# -----------
|
||||||
# Permissions evaluated in scripted ways
|
# Permissions evaluated in scripted ways
|
||||||
|
|
||||||
# permission_query_conditions = {
|
permission_query_conditions = {
|
||||||
# "Event": "frappe.desk.doctype.event.event.get_permission_query_conditions",
|
"LMS Certificate": "lms.lms.doctype.lms_certificate.lms_certificate.get_permission_query_conditions",
|
||||||
# }
|
}
|
||||||
#
|
|
||||||
# has_permission = {
|
has_permission = {
|
||||||
# "Event": "frappe.desk.doctype.event.event.has_permission",
|
"LMS Live Class": "lms.lms.doctype.lms_live_class.lms_live_class.has_permission",
|
||||||
# }
|
"LMS Batch": "lms.lms.doctype.lms_batch.lms_batch.has_permission",
|
||||||
|
"LMS Program": "lms.lms.doctype.lms_program.lms_program.has_permission",
|
||||||
|
"LMS Certificate": "lms.lms.doctype.lms_certificate.lms_certificate.has_permission",
|
||||||
|
}
|
||||||
|
|
||||||
# DocType Class
|
# DocType Class
|
||||||
# ---------------
|
# ---------------
|
||||||
|
|||||||
@@ -130,7 +130,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2025-12-02 16:58:49.903274",
|
"modified": "2026-02-19 14:26:14.027340",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "Job",
|
"module": "Job",
|
||||||
"name": "Job Opportunity",
|
"name": "Job Opportunity",
|
||||||
@@ -149,24 +149,16 @@
|
|||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "LMS Student",
|
|
||||||
"select": 1,
|
|
||||||
"share": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
"if_owner": 1,
|
"if_owner": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "LMS Student",
|
"role": "LMS Student",
|
||||||
|
"select": 1,
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
// Copyright (c) 2022, Frappe and contributors
|
|
||||||
// For license information, please see license.txt
|
|
||||||
|
|
||||||
frappe.ui.form.on("Job Settings", {
|
|
||||||
// refresh: function(frm) {
|
|
||||||
// }
|
|
||||||
});
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"actions": [],
|
|
||||||
"allow_rename": 1,
|
|
||||||
"creation": "2022-02-07 12:01:41.422955",
|
|
||||||
"doctype": "DocType",
|
|
||||||
"editable_grid": 1,
|
|
||||||
"engine": "InnoDB",
|
|
||||||
"field_order": [
|
|
||||||
"allow_posting",
|
|
||||||
"title",
|
|
||||||
"subtitle"
|
|
||||||
],
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "allow_posting",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Allow Job Posting From Website"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "title",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Job Board Title"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"fieldname": "subtitle",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"label": "Job Board Subtitle"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index_web_pages_for_search": 1,
|
|
||||||
"issingle": 1,
|
|
||||||
"links": [],
|
|
||||||
"modified": "2022-02-11 15:56:38.958317",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "Job",
|
|
||||||
"name": "Job Settings",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sort_field": "modified",
|
|
||||||
"sort_order": "DESC",
|
|
||||||
"states": []
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Copyright (c) 2022, Frappe and contributors
|
|
||||||
# For license information, please see license.txt
|
|
||||||
|
|
||||||
# import frappe
|
|
||||||
from frappe.model.document import Document
|
|
||||||
|
|
||||||
|
|
||||||
class JobSettings(Document):
|
|
||||||
pass
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
# Copyright (c) 2022, Frappe and Contributors
|
|
||||||
# See license.txt
|
|
||||||
|
|
||||||
# import frappe
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
|
|
||||||
class TestJobSettings(unittest.TestCase):
|
|
||||||
pass
|
|
||||||
+27
-9
@@ -31,6 +31,7 @@ from pypika import functions as fn
|
|||||||
|
|
||||||
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
||||||
from lms.lms.utils import (
|
from lms.lms.utils import (
|
||||||
|
LMS_ROLES,
|
||||||
can_modify_batch,
|
can_modify_batch,
|
||||||
can_modify_course,
|
can_modify_course,
|
||||||
get_average_rating,
|
get_average_rating,
|
||||||
@@ -41,6 +42,7 @@ from lms.lms.utils import (
|
|||||||
get_lms_route,
|
get_lms_route,
|
||||||
has_course_instructor_role,
|
has_course_instructor_role,
|
||||||
has_evaluator_role,
|
has_evaluator_role,
|
||||||
|
has_lms_role,
|
||||||
has_moderator_role,
|
has_moderator_role,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -606,12 +608,7 @@ def check_app_permission():
|
|||||||
if frappe.session.user == "Administrator":
|
if frappe.session.user == "Administrator":
|
||||||
return True
|
return True
|
||||||
|
|
||||||
roles = frappe.get_roles()
|
return has_lms_role()
|
||||||
lms_roles = ["Moderator", "Course Creator", "Batch Evaluator", "LMS Student"]
|
|
||||||
if any(role in roles for role in lms_roles):
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -1296,6 +1293,7 @@ def get_lms_settings():
|
|||||||
"contact_us_url",
|
"contact_us_url",
|
||||||
"livecode_url",
|
"livecode_url",
|
||||||
"disable_pwa",
|
"disable_pwa",
|
||||||
|
"allow_job_posting",
|
||||||
]
|
]
|
||||||
|
|
||||||
settings = frappe._dict()
|
settings = frappe._dict()
|
||||||
@@ -1308,7 +1306,6 @@ def get_lms_settings():
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def cancel_evaluation(evaluation: dict):
|
def cancel_evaluation(evaluation: dict):
|
||||||
evaluation = frappe._dict(evaluation)
|
evaluation = frappe._dict(evaluation)
|
||||||
print(evaluation.member, frappe.session.user)
|
|
||||||
if evaluation.member != frappe.session.user:
|
if evaluation.member != frappe.session.user:
|
||||||
frappe.throw(_("You do not have permission to cancel this evaluation."), frappe.PermissionError)
|
frappe.throw(_("You do not have permission to cancel this evaluation."), frappe.PermissionError)
|
||||||
|
|
||||||
@@ -1369,6 +1366,9 @@ def get_certification_details(course: str):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def save_role(user: str, role: str, value: int):
|
def save_role(user: str, role: str, value: int):
|
||||||
frappe.only_for("Moderator")
|
frappe.only_for("Moderator")
|
||||||
|
if role not in LMS_ROLES:
|
||||||
|
frappe.throw(_("You do not have permission to modify this role."), frappe.PermissionError)
|
||||||
|
|
||||||
if cint(value):
|
if cint(value):
|
||||||
doc = frappe.get_doc(
|
doc = frappe.get_doc(
|
||||||
{
|
{
|
||||||
@@ -1716,8 +1716,12 @@ def get_profile_details(username: str):
|
|||||||
],
|
],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
roles = frappe.get_roles(details.name)
|
||||||
details.roles = frappe.get_roles(details.name)
|
if not has_lms_role():
|
||||||
|
frappe.throw(
|
||||||
|
_("User does not have permission to access this user's profile details."), frappe.PermissionError
|
||||||
|
)
|
||||||
|
details.roles = roles
|
||||||
return details
|
return details
|
||||||
|
|
||||||
|
|
||||||
@@ -2204,3 +2208,17 @@ def get_assessment_from_lesson(course: str, assessmentType: str):
|
|||||||
assessments.append(quiz_name)
|
assessments.append(quiz_name)
|
||||||
|
|
||||||
return assessments
|
return assessments
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_badges(member: str):
|
||||||
|
if not has_lms_role():
|
||||||
|
frappe.throw(_("You do not have permission to access badges."), frappe.PermissionError)
|
||||||
|
|
||||||
|
badges = frappe.get_all(
|
||||||
|
"LMS Badge Assignment",
|
||||||
|
{"member": member},
|
||||||
|
["name", "member", "badge", "badge_image", "badge_description", "issued_on"],
|
||||||
|
)
|
||||||
|
|
||||||
|
return badges
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ frappe.ui.form.on("LMS Badge", {
|
|||||||
refresh: (frm) => {
|
refresh: (frm) => {
|
||||||
frm.events.set_field_options(frm);
|
frm.events.set_field_options(frm);
|
||||||
|
|
||||||
if (frm.doc.event == "Auto Assign") {
|
if (frm.doc.event == "Manual Assignment" && frm.doc.enabled) {
|
||||||
add_assign_button(frm);
|
add_assign_button(frm);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -49,11 +49,13 @@ const add_assign_button = (frm) => {
|
|||||||
frappe.call({
|
frappe.call({
|
||||||
method: "lms.lms.doctype.lms_badge.lms_badge.assign_badge",
|
method: "lms.lms.doctype.lms_badge.lms_badge.assign_badge",
|
||||||
args: {
|
args: {
|
||||||
badge: frm.doc,
|
badge_name: frm.doc.name,
|
||||||
},
|
},
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (r.message) {
|
if (r.message == "success") {
|
||||||
frappe.msgprint(r.message);
|
frappe.toast(__("Badge assigned successfully"));
|
||||||
|
} else {
|
||||||
|
frappe.toast(__("Failed to assign badge"));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -52,14 +52,14 @@
|
|||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Event",
|
"label": "Event",
|
||||||
"options": "New\nValue Change\nAuto Assign",
|
"options": "New\nValue Change\nManual Assignment",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "condition",
|
"fieldname": "condition",
|
||||||
"fieldtype": "Code",
|
"fieldtype": "Code",
|
||||||
"label": "Condition",
|
"label": "Condition",
|
||||||
"mandatory_depends_on": "eval:doc.event == \"Auto Assign\""
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.event == 'Value Change'",
|
"depends_on": "eval:doc.event == 'Value Change'",
|
||||||
@@ -100,7 +100,7 @@
|
|||||||
"link_fieldname": "badge"
|
"link_fieldname": "badge"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2026-02-03 10:52:37.122370",
|
"modified": "2026-02-20 17:58:25.924109",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Badge",
|
"name": "LMS Badge",
|
||||||
@@ -131,15 +131,6 @@
|
|||||||
"role": "Moderator",
|
"role": "Moderator",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
|
||||||
{
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "LMS Student",
|
|
||||||
"share": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"row_format": "Dynamic",
|
"row_format": "Dynamic",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from frappe.model.document import Document
|
|||||||
|
|
||||||
class LMSBadge(Document):
|
class LMSBadge(Document):
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
if self.event == "Auto Assign" and self.condition:
|
if self.event == "Manual Assignment" and self.condition:
|
||||||
try:
|
try:
|
||||||
json.loads(self.condition)
|
json.loads(self.condition)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@@ -54,6 +54,7 @@ def award(doc, member):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
assignment.save()
|
assignment.save()
|
||||||
|
return assignment.name
|
||||||
|
|
||||||
|
|
||||||
def eval_condition(doc, condition):
|
def eval_condition(doc, condition):
|
||||||
@@ -61,16 +62,30 @@ def eval_condition(doc, condition):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def assign_badge(badge: str, user: str):
|
def assign_badge(badge_name: str):
|
||||||
badge = frappe._dict(json.loads(badge))
|
assignments = []
|
||||||
if not badge.event == "Auto Assign":
|
badge = frappe.db.get_value(
|
||||||
|
"LMS Badge",
|
||||||
|
badge_name,
|
||||||
|
["name", "event", "reference_doctype", "condition", "user_field"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
if not badge:
|
||||||
|
frappe.throw(_("Badge {0} not found").format(badge_name), frappe.DoesNotExistError)
|
||||||
|
|
||||||
|
if not badge.event == "Manual Assignment":
|
||||||
return
|
return
|
||||||
|
|
||||||
fields = ["name"]
|
fields = ["name"]
|
||||||
fields.append(badge.user_field)
|
fields.append(badge.user_field)
|
||||||
list = frappe.get_all(badge.reference_doctype, filters=badge.condition, fields=fields)
|
docs = frappe.get_all(badge.reference_doctype, filters=json.loads(badge.condition), fields=fields)
|
||||||
for doc in list:
|
|
||||||
award(badge, doc.get(badge.user_field))
|
for doc in docs:
|
||||||
|
assignment_name = award(badge, doc.get(badge.user_field))
|
||||||
|
if assignment_name:
|
||||||
|
assignments.append(assignment_name)
|
||||||
|
|
||||||
|
return "success" if assignments else "failed"
|
||||||
|
|
||||||
|
|
||||||
def process_badges(doc, state):
|
def process_badges(doc, state):
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-12-04 17:06:26.090276",
|
"modified": "2026-02-19 15:06:08.389081",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Badge Assignment",
|
"name": "LMS Badge Assignment",
|
||||||
@@ -120,10 +120,6 @@
|
|||||||
"read": 1,
|
"read": 1,
|
||||||
"role": "LMS Student"
|
"role": "LMS Student"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"read": 1,
|
|
||||||
"role": "LMS Student"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
|
|||||||
@@ -10,17 +10,17 @@ from lms.lms.doctype.lms_badge.lms_badge import eval_condition
|
|||||||
|
|
||||||
class LMSBadgeAssignment(Document):
|
class LMSBadgeAssignment(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_owner()
|
|
||||||
self.validate_duplicate_badge_assignment()
|
self.validate_duplicate_badge_assignment()
|
||||||
self.validate_badge_criteria()
|
self.validate_badge_criteria()
|
||||||
|
self.validate_owner()
|
||||||
|
|
||||||
def validate_owner(self):
|
def validate_owner(self):
|
||||||
if self.owner == self.member:
|
event = frappe.db.get_value("LMS Badge", self.badge, "event")
|
||||||
return
|
if event == "Manual Assignment":
|
||||||
|
roles = frappe.get_roles(frappe.session.user)
|
||||||
roles = frappe.get_roles(self.owner)
|
admins = ["Moderator", "Course Creator", "Batch Evaluator"]
|
||||||
if "Moderator" not in roles:
|
if not any(role in roles for role in admins):
|
||||||
frappe.throw(_("You must be a Moderator to assign badges to users."))
|
frappe.throw(_("You must be an Admin to assign badges to users."))
|
||||||
|
|
||||||
def validate_duplicate_badge_assignment(self):
|
def validate_duplicate_badge_assignment(self):
|
||||||
grant_only_once = frappe.db.get_value("LMS Badge", self.badge, "grant_only_once")
|
grant_only_once = frappe.db.get_value("LMS Badge", self.badge, "grant_only_once")
|
||||||
@@ -40,25 +40,27 @@ class LMSBadgeAssignment(Document):
|
|||||||
"LMS Badge", self.badge, ["reference_doctype", "user_field", "condition", "enabled"], as_dict=True
|
"LMS Badge", self.badge, ["reference_doctype", "user_field", "condition", "enabled"], as_dict=True
|
||||||
)
|
)
|
||||||
|
|
||||||
if badge_details:
|
if not badge_details:
|
||||||
if badge_details.reference_doctype and badge_details.user_field and badge_details.condition:
|
return
|
||||||
user_fieldname = frappe.db.get_value(
|
|
||||||
"DocField",
|
if badge_details.reference_doctype and badge_details.user_field and badge_details.condition:
|
||||||
{"parent": badge_details.reference_doctype, "fieldname": badge_details.user_field},
|
user_fieldname = frappe.db.get_value(
|
||||||
"fieldname",
|
"DocField",
|
||||||
|
{"parent": badge_details.reference_doctype, "fieldname": badge_details.user_field},
|
||||||
|
"fieldname",
|
||||||
|
)
|
||||||
|
|
||||||
|
documents = frappe.get_all(
|
||||||
|
badge_details.reference_doctype,
|
||||||
|
{user_fieldname: self.member},
|
||||||
|
)
|
||||||
|
|
||||||
|
for document in documents:
|
||||||
|
reference_value = eval_condition(
|
||||||
|
frappe.get_doc(badge_details.reference_doctype, document.name),
|
||||||
|
badge_details.condition,
|
||||||
)
|
)
|
||||||
|
if reference_value:
|
||||||
|
return
|
||||||
|
|
||||||
documents = frappe.get_all(
|
frappe.throw(_("Member does not meet the criteria for the badge {0}.").format(self.badge))
|
||||||
badge_details.reference_doctype,
|
|
||||||
{user_fieldname: self.member},
|
|
||||||
)
|
|
||||||
|
|
||||||
for document in documents:
|
|
||||||
reference_value = eval_condition(
|
|
||||||
frappe.get_doc(badge_details.reference_doctype, document.name),
|
|
||||||
badge_details.condition,
|
|
||||||
)
|
|
||||||
if reference_value:
|
|
||||||
return
|
|
||||||
|
|
||||||
frappe.throw(_("Member does not meet the criteria for the badge {0}.").format(self.badge))
|
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ from lms.lms.utils import (
|
|||||||
get_lesson_url,
|
get_lesson_url,
|
||||||
get_lms_route,
|
get_lms_route,
|
||||||
get_quiz_details,
|
get_quiz_details,
|
||||||
|
guest_access_allowed,
|
||||||
update_payment_record,
|
update_payment_record,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -213,6 +214,10 @@ def create_live_class(
|
|||||||
auto_recording: str,
|
auto_recording: str,
|
||||||
description: str = None,
|
description: str = None,
|
||||||
):
|
):
|
||||||
|
roles = frappe.get_roles()
|
||||||
|
if not any(role in roles for role in ["Moderator", "Batch Evaluator"]):
|
||||||
|
frappe.throw(_("You do not have permission to create a live class."))
|
||||||
|
|
||||||
payload = {
|
payload = {
|
||||||
"topic": title,
|
"topic": title,
|
||||||
"start_time": format_datetime(f"{date} {time}", "yyyy-MM-ddTHH:mm:ssZ"),
|
"start_time": format_datetime(f"{date} {time}", "yyyy-MM-ddTHH:mm:ssZ"),
|
||||||
@@ -391,3 +396,26 @@ def send_mail(batch, student):
|
|||||||
args=args,
|
args=args,
|
||||||
header=[_(f"Batch Start Reminder: {batch.title}"), "orange"],
|
header=[_(f"Batch Start Reminder: {batch.title}"), "orange"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_permission(doc, ptype="read", user=None):
|
||||||
|
user = user or frappe.session.user
|
||||||
|
if user == "Guest" and not guest_access_allowed():
|
||||||
|
return False
|
||||||
|
|
||||||
|
roles = frappe.get_roles(user)
|
||||||
|
if "Moderator" in roles or "Batch Evaluator" in roles:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if ptype not in ("read", "select", "print"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
is_enrolled = frappe.db.exists("LMS Batch Enrollment", {"batch": doc.name, "member": user})
|
||||||
|
if is_enrolled:
|
||||||
|
return True
|
||||||
|
|
||||||
|
is_batch_published = frappe.db.get_value("LMS Batch", doc.name, "published")
|
||||||
|
if is_batch_published:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-12-17 16:50:31.128747",
|
"modified": "2026-02-20 17:32:34.580862",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate",
|
"name": "LMS Certificate",
|
||||||
@@ -153,27 +153,6 @@
|
|||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"if_owner": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "LMS Student",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "LMS Student",
|
|
||||||
"share": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
@@ -197,6 +176,15 @@
|
|||||||
"role": "Course Creator",
|
"role": "Course Creator",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "LMS Student",
|
||||||
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"row_format": "Dynamic",
|
"row_format": "Dynamic",
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from frappe.utils.telemetry import capture
|
|||||||
|
|
||||||
class LMSCertificate(Document):
|
class LMSCertificate(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.validate_criteria()
|
||||||
self.validate_duplicate_certificate()
|
self.validate_duplicate_certificate()
|
||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
@@ -54,6 +55,43 @@ class LMSCertificate(Document):
|
|||||||
header=[subject, "green"],
|
header=[subject, "green"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def validate_criteria(self):
|
||||||
|
self.validate_role_of_owner()
|
||||||
|
self.validate_batch_enrollment()
|
||||||
|
self.validate_course_enrollment()
|
||||||
|
|
||||||
|
def validate_role_of_owner(self):
|
||||||
|
roles = frappe.get_roles()
|
||||||
|
is_admin = any(role in roles for role in ["Moderator", "Course Creator", "Batch Evaluator"])
|
||||||
|
if not self.course and not self.batch_name and not is_admin:
|
||||||
|
frappe.throw(_("Course or Batch is required to issue a certificate."))
|
||||||
|
|
||||||
|
def validate_batch_enrollment(self):
|
||||||
|
if self.batch_name:
|
||||||
|
is_enrolled = frappe.db.exists(
|
||||||
|
"LMS Batch Enrollment", {"batch": self.batch_name, "member": self.member}
|
||||||
|
)
|
||||||
|
if not is_enrolled:
|
||||||
|
frappe.throw(_("Certification cannot be issued as the member is not enrolled in this batch."))
|
||||||
|
|
||||||
|
def validate_course_enrollment(self):
|
||||||
|
if self.course:
|
||||||
|
is_enrolled = frappe.db.exists("LMS Enrollment", {"course": self.course, "member": self.member})
|
||||||
|
if not is_enrolled:
|
||||||
|
frappe.throw(
|
||||||
|
_("Certification cannot be issued as the member is not enrolled in this course.")
|
||||||
|
)
|
||||||
|
|
||||||
|
completion_certificate = frappe.db.get_value("LMS Course", self.course, "enable_certification")
|
||||||
|
if completion_certificate:
|
||||||
|
progress = frappe.db.get_value(
|
||||||
|
"LMS Enrollment", {"course": self.course, "member": self.member}, "progress"
|
||||||
|
)
|
||||||
|
if progress < 100:
|
||||||
|
frappe.throw(
|
||||||
|
_("Certification cannot be issued as the member has not completed the course.")
|
||||||
|
)
|
||||||
|
|
||||||
def validate_duplicate_certificate(self):
|
def validate_duplicate_certificate(self):
|
||||||
self.validate_course_duplicates()
|
self.validate_course_duplicates()
|
||||||
self.validate_batch_duplicates()
|
self.validate_batch_duplicates()
|
||||||
@@ -177,3 +215,23 @@ def validate_certification_eligibility(course):
|
|||||||
)
|
)
|
||||||
if progress < 100:
|
if progress < 100:
|
||||||
frappe.throw(_("You have not completed the course yet."))
|
frappe.throw(_("You have not completed the course yet."))
|
||||||
|
|
||||||
|
|
||||||
|
def has_permission(doc, ptype="read", user=None):
|
||||||
|
user = user or frappe.session.user
|
||||||
|
roles = frappe.get_roles(user)
|
||||||
|
if "Moderator" in roles or "Course Creator" in roles or "Batch Evaluator" in roles:
|
||||||
|
return True
|
||||||
|
if doc.owner == user:
|
||||||
|
return True
|
||||||
|
if ptype not in ("read", "select", "print"):
|
||||||
|
return False
|
||||||
|
return doc.published
|
||||||
|
|
||||||
|
|
||||||
|
def get_permission_query_conditions(user):
|
||||||
|
user = user or frappe.session.user
|
||||||
|
roles = frappe.get_roles(user)
|
||||||
|
if "Moderator" in roles or "Course Creator" in roles or "Batch Evaluator" in roles:
|
||||||
|
return None
|
||||||
|
return """(`tabLMS Certificate`.published = 1)"""
|
||||||
|
|||||||
@@ -157,7 +157,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-11-10 11:40:50.679211",
|
"modified": "2026-02-19 16:01:43.810407",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Certificate Request",
|
"name": "LMS Certificate Request",
|
||||||
@@ -192,6 +192,7 @@
|
|||||||
"create": 1,
|
"create": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
|
"if_owner": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
|
|||||||
@@ -41,7 +41,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-29 16:10:47.787285",
|
"modified": "2026-02-20 17:40:39.823017",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Course Review",
|
"name": "LMS Course Review",
|
||||||
@@ -63,8 +63,8 @@
|
|||||||
"create": 1,
|
"create": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
|
"if_owner": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "LMS Student",
|
"role": "LMS Student",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from frappe.utils import ceil
|
|||||||
|
|
||||||
|
|
||||||
class LMSEnrollment(Document):
|
class LMSEnrollment(Document):
|
||||||
def validate(self):
|
def before_insert(self):
|
||||||
self.validate_duplicate_enrollment()
|
self.validate_duplicate_enrollment()
|
||||||
self.validate_course_enrollment_eligibility()
|
self.validate_course_enrollment_eligibility()
|
||||||
self.validate_owner()
|
self.validate_owner()
|
||||||
@@ -27,6 +27,7 @@ class LMSEnrollment(Document):
|
|||||||
{
|
{
|
||||||
"course": self.course,
|
"course": self.course,
|
||||||
"member": self.member,
|
"member": self.member,
|
||||||
|
"name": ["!=", self.name],
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -49,7 +50,10 @@ class LMSEnrollment(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if self.enrollment_from_batch:
|
if self.enrollment_from_batch:
|
||||||
return
|
if frappe.db.exists(
|
||||||
|
"LMS Batch Enrollment", {"batch": self.enrollment_from_batch, "member": self.member}
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
if not course_details.published and not is_admin():
|
if not course_details.published and not is_admin():
|
||||||
frappe.throw(_("You cannot enroll in an unpublished course."))
|
frappe.throw(_("You cannot enroll in an unpublished course."))
|
||||||
|
|||||||
@@ -169,3 +169,18 @@ def get_minutes(duration_in_seconds):
|
|||||||
if duration_in_seconds:
|
if duration_in_seconds:
|
||||||
return int(duration_in_seconds) // 60
|
return int(duration_in_seconds) // 60
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def has_permission(doc, ptype="read", user=None):
|
||||||
|
user = user or frappe.session.user
|
||||||
|
roles = frappe.get_roles(user)
|
||||||
|
if "Moderator" in roles or "Batch Evaluator" in roles:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if ptype not in ("read", "select", "print"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
return frappe.db.exists(
|
||||||
|
"LMS Batch Enrollment",
|
||||||
|
{"batch": doc.batch_name, "member": user},
|
||||||
|
)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
from lms.lms.utils import guest_access_allowed
|
||||||
|
|
||||||
|
|
||||||
class LMSProgram(Document):
|
class LMSProgram(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@@ -41,3 +43,27 @@ class LMSProgram(Document):
|
|||||||
|
|
||||||
if self.member_count != member_count:
|
if self.member_count != member_count:
|
||||||
self.member_count = member_count
|
self.member_count = member_count
|
||||||
|
|
||||||
|
|
||||||
|
def has_permission(doc, ptype="read", user=None):
|
||||||
|
user = user or frappe.session.user
|
||||||
|
|
||||||
|
if user == "Guest" and not guest_access_allowed():
|
||||||
|
return False
|
||||||
|
|
||||||
|
roles = frappe.get_roles(user)
|
||||||
|
if "Moderator" in roles or "Course Creator" in roles:
|
||||||
|
return True
|
||||||
|
|
||||||
|
if ptype not in ("read", "select", "print"):
|
||||||
|
return False
|
||||||
|
|
||||||
|
is_enrolled = frappe.db.exists("LMS Program Member", {"parent": doc.name, "member": user})
|
||||||
|
if is_enrolled:
|
||||||
|
return True
|
||||||
|
|
||||||
|
is_program_published = frappe.db.get_value("LMS Program", doc.name, "published")
|
||||||
|
if is_program_published:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|||||||
+2
-1
@@ -88,7 +88,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-06-24 14:42:08.288983",
|
"modified": "2026-02-20 14:43:56.587110",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Programming Exercise Submission",
|
"name": "LMS Programming Exercise Submission",
|
||||||
@@ -146,6 +146,7 @@
|
|||||||
"create": 1,
|
"create": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 1,
|
"export": 1,
|
||||||
|
"if_owner": 1,
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
|
|||||||
@@ -113,11 +113,12 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"grid_page_length": 50,
|
||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2024-10-07 16:52:04.162521",
|
"modified": "2026-02-19 16:31:23.401819",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Quiz Submission",
|
"name": "LMS Quiz Submission",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
@@ -133,21 +134,12 @@
|
|||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "LMS Student",
|
|
||||||
"share": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "member_name",
|
"title_field": "member_name",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,7 +76,9 @@
|
|||||||
"contact_us_tab",
|
"contact_us_tab",
|
||||||
"contact_us_email",
|
"contact_us_email",
|
||||||
"column_break_gcgv",
|
"column_break_gcgv",
|
||||||
"contact_us_url"
|
"contact_us_url",
|
||||||
|
"jobs_tab",
|
||||||
|
"allow_job_posting"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -471,13 +473,24 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_dtns",
|
"fieldname": "column_break_dtns",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "jobs_tab",
|
||||||
|
"fieldtype": "Tab Break",
|
||||||
|
"label": "Jobs"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "1",
|
||||||
|
"fieldname": "allow_job_posting",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Allow Job Posting"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-01-01 19:36:54.443390",
|
"modified": "2026-02-19 16:28:15.310145",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Settings",
|
"name": "LMS Settings",
|
||||||
@@ -493,13 +506,6 @@
|
|||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"email": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"role": "LMS Student",
|
|
||||||
"share": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
|
|||||||
@@ -102,7 +102,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-07-30 14:38:52.555010",
|
"modified": "2026-02-20 12:02:29.458645",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Video Watch Duration",
|
"name": "LMS Video Watch Duration",
|
||||||
@@ -120,17 +120,6 @@
|
|||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "LMS Student",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
@@ -154,6 +143,18 @@
|
|||||||
"role": "Course Creator",
|
"role": "Course Creator",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"if_owner": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "LMS Student",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"row_format": "Dynamic",
|
"row_format": "Dynamic",
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ from pypika import functions as fn
|
|||||||
from lms.lms.md import find_macros
|
from lms.lms.md import find_macros
|
||||||
|
|
||||||
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
|
RE_SLUG_NOTALLOWED = re.compile("[^a-z0-9]+")
|
||||||
|
LMS_ROLES = ["Moderator", "Course Creator", "Batch Evaluator", "LMS Student"]
|
||||||
|
|
||||||
|
|
||||||
def get_lms_path():
|
def get_lms_path():
|
||||||
@@ -1209,6 +1210,9 @@ def get_country_code():
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_question_details(question: str) -> dict:
|
def get_question_details(question: str) -> dict:
|
||||||
|
if not has_lms_role():
|
||||||
|
frappe.throw(_("You are not authorized to view the question details."))
|
||||||
|
|
||||||
fields = ["question", "type", "multiple"]
|
fields = ["question", "type", "multiple"]
|
||||||
for i in range(1, 5):
|
for i in range(1, 5):
|
||||||
fields.append(f"option_{i}")
|
fields.append(f"option_{i}")
|
||||||
@@ -1239,6 +1243,10 @@ def get_batch_courses(batch: str) -> list:
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_assessments(batch: str) -> list:
|
def get_assessments(batch: str) -> list:
|
||||||
member = frappe.session.user
|
member = frappe.session.user
|
||||||
|
is_enrolled = frappe.db.exists("LMS Batch Enrollment", {"batch": batch, "member": member})
|
||||||
|
if not is_enrolled and not can_modify_batch(batch):
|
||||||
|
frappe.throw(_("You are not authorized to view the assessments of this batch."))
|
||||||
|
|
||||||
assessments = frappe.get_all(
|
assessments = frappe.get_all(
|
||||||
"LMS Assessment",
|
"LMS Assessment",
|
||||||
{"parent": batch},
|
{"parent": batch},
|
||||||
@@ -2297,3 +2305,10 @@ def can_modify_batch(batch: str) -> bool:
|
|||||||
if not (has_moderator_role() or is_instructor):
|
if not (has_moderator_role() or is_instructor):
|
||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def has_lms_role():
|
||||||
|
roles = frappe.get_roles()
|
||||||
|
lms_roles = set(LMS_ROLES)
|
||||||
|
user_roles = set(roles)
|
||||||
|
return not lms_roles.isdisjoint(user_roles)
|
||||||
|
|||||||
+3
-1
@@ -117,4 +117,6 @@ lms.patches.v2_0.fix_job_application_resume_urls
|
|||||||
lms.patches.v2_0.open_to_opportunities
|
lms.patches.v2_0.open_to_opportunities
|
||||||
lms.patches.v2_0.open_to_work
|
lms.patches.v2_0.open_to_work
|
||||||
lms.patches.v2_0.share_enrollment
|
lms.patches.v2_0.share_enrollment
|
||||||
lms.patches.v2_0.give_user_list_permission #11-02-2026
|
lms.patches.v2_0.give_user_list_permission #11-02-2026
|
||||||
|
lms.patches.v2_0.rename_badge_assignment_event
|
||||||
|
lms.patches.v2_0.enable_allow_job_posting
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.db.set_single_value("LMS Settings", "allow_job_posting", 1)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
badge_with_auto_assign = frappe.get_all("LMS Badge", filters={"event": "Auto Assign"}, fields=["name"])
|
||||||
|
for badge in badge_with_auto_assign:
|
||||||
|
frappe.db.set_value("LMS Badge", badge.name, "event", "Manual Assignment")
|
||||||
Reference in New Issue
Block a user