Merge remote-tracking branch 'upstream/develop' into fix/course-deletion

This commit is contained in:
raizasafeel
2026-01-22 16:33:07 +05:30
91 changed files with 11278 additions and 8036 deletions
+3
View File
@@ -71,6 +71,9 @@ jobs:
- name: setup requirements - name: setup requirements
working-directory: /home/runner/frappe-bench working-directory: /home/runner/frappe-bench
run: bench setup requirements --dev run: bench setup requirements --dev
- name: block endpoints
working-directory: /home/runner/frappe-bench
run: bench --site frappe.local set-config block_endpoints 1
- name: allow tests - name: allow tests
working-directory: /home/runner/frappe-bench working-directory: /home/runner/frappe-bench
run: bench --site frappe.local set-config allow_tests true run: bench --site frappe.local set-config allow_tests true
+1 -3
View File
@@ -10,9 +10,8 @@
<script setup> <script setup>
import { FrappeUIProvider } from 'frappe-ui' import { FrappeUIProvider } from 'frappe-ui'
import { Dialogs } from '@/utils/dialogs' import { Dialogs } from '@/utils/dialogs'
import { computed, onUnmounted, ref, watch } from 'vue' import { computed, onUnmounted, ref } from 'vue'
import { useScreenSize } from './utils/composables' import { useScreenSize } from './utils/composables'
import { usersStore } from '@/stores/user'
import { useSettings } from '@/stores/settings' import { useSettings } from '@/stores/settings'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import DesktopLayout from './components/DesktopLayout.vue' import DesktopLayout from './components/DesktopLayout.vue'
@@ -23,7 +22,6 @@ import InstallPrompt from './components/InstallPrompt.vue'
const { isMobile } = useScreenSize() const { isMobile } = useScreenSize()
const router = useRouter() const router = useRouter()
const noSidebar = ref(false) const noSidebar = ref(false)
const { userResource } = usersStore()
const { settings } = useSettings() const { settings } = useSettings()
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
-152
View File
@@ -1,152 +0,0 @@
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 100;
font-display: swap;
src: url("Inter-Thin.woff2?v=3.12") format("woff2"),
url("Inter-Thin.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 100;
font-display: swap;
src: url("Inter-ThinItalic.woff2?v=3.12") format("woff2"),
url("Inter-ThinItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 200;
font-display: swap;
src: url("Inter-ExtraLight.woff2?v=3.12") format("woff2"),
url("Inter-ExtraLight.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 200;
font-display: swap;
src: url("Inter-ExtraLightItalic.woff2?v=3.12") format("woff2"),
url("Inter-ExtraLightItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url("Inter-Light.woff2?v=3.12") format("woff2"),
url("Inter-Light.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 300;
font-display: swap;
src: url("Inter-LightItalic.woff2?v=3.12") format("woff2"),
url("Inter-LightItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url("Inter-Regular.woff2?v=3.12") format("woff2"),
url("Inter-Regular.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 400;
font-display: swap;
src: url("Inter-Italic.woff2?v=3.12") format("woff2"),
url("Inter-Italic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url("Inter-Medium.woff2?v=3.12") format("woff2"),
url("Inter-Medium.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 500;
font-display: swap;
src: url("Inter-MediumItalic.woff2?v=3.12") format("woff2"),
url("Inter-MediumItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url("Inter-SemiBold.woff2?v=3.12") format("woff2"),
url("Inter-SemiBold.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 600;
font-display: swap;
src: url("Inter-SemiBoldItalic.woff2?v=3.12") format("woff2"),
url("Inter-SemiBoldItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url("Inter-Bold.woff2?v=3.12") format("woff2"),
url("Inter-Bold.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 700;
font-display: swap;
src: url("Inter-BoldItalic.woff2?v=3.12") format("woff2"),
url("Inter-BoldItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 800;
font-display: swap;
src: url("Inter-ExtraBold.woff2?v=3.12") format("woff2"),
url("Inter-ExtraBold.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 800;
font-display: swap;
src: url("Inter-ExtraBoldItalic.woff2?v=3.12") format("woff2"),
url("Inter-ExtraBoldItalic.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 900;
font-display: swap;
src: url("Inter-Black.woff2?v=3.12") format("woff2"),
url("Inter-Black.woff?v=3.12") format("woff");
}
@font-face {
font-family: 'Inter';
font-style: italic;
font-weight: 900;
font-display: swap;
src: url("Inter-BlackItalic.woff2?v=3.12") format("woff2"),
url("Inter-BlackItalic.woff?v=3.12") format("woff");
}
+3 -3
View File
@@ -43,7 +43,7 @@
<ListRow <ListRow
:row="row" :row="row"
v-for="row in students.data" v-for="row in students.data"
class="group cursor-pointer" class="group cursor-pointer hover:bg-surface-gray-2 rounded"
@click="openStudentProgressModal(row)" @click="openStudentProgressModal(row)"
> >
<template #default="{ column, item }"> <template #default="{ column, item }">
@@ -88,7 +88,7 @@
</div> </div>
</template> </template>
</ListSelectBanner> </ListSelectBanner>
<div class="mt-4" v-if="students.hasNextPage"> <div class="mt-4 flex justify-center" v-if="students.hasNextPage">
<Button @click="students.next()"> <Button @click="students.next()">
{{ __('Load More') }} {{ __('Load More') }}
</Button> </Button>
@@ -170,7 +170,7 @@ const studentColumns = [
{ {
label: 'Full Name', label: 'Full Name',
key: 'full_name', key: 'full_name',
width: '20rem', width: '25rem',
icon: 'user', icon: 'user',
}, },
{ {
@@ -90,7 +90,7 @@
<FormControl <FormControl
v-model="profile.open_to" v-model="profile.open_to"
type="select" type="select"
:options="[' ', 'Opportunities', 'Hiring']" :options="[' ', 'Work', 'Hiring']"
:label="__('Open to')" :label="__('Open to')"
:placeholder="__('Looking for new work or hiring talent?')" :placeholder="__('Looking for new work or hiring talent?')"
/> />
@@ -2,21 +2,25 @@
<Dialog <Dialog
v-model="show" v-model="show"
:options="{ :options="{
title:
gatewayID === 'new'
? __('New Payment Gateway')
: __('Edit Payment Gateway'),
size: '3xl', size: '3xl',
}" }"
> >
<template #body-header>
<div class="text-lg font-semibold">
{{
gatewayID === 'new'
? __('New Payment Gateway')
: __('Edit Payment Gateway')
}}
</div>
</template>
<template #body-content> <template #body-content>
<SettingFields <SettingFields
v-if="gatewayID != 'new' && paymentGateway.data" v-if="gatewayID != 'new' && paymentGateway.data"
:fields="paymentGateway.data.fields" :sections="paymentGateway.data.sections"
:data="paymentGateway.data.data" :data="paymentGateway.data.data"
class="pt-5 my-0"
/> />
<div v-else> <div v-else class="mt-5">
<FormControl <FormControl
v-model="newGateway" v-model="newGateway"
:label="__('Select Payment Gateway')" :label="__('Select Payment Gateway')"
@@ -26,9 +30,8 @@
/> />
<SettingFields <SettingFields
v-if="newGateway" v-if="newGateway"
:fields="newGatewayFields" :sections="newGatewayFields"
:data="newGatewayData" :data="newGatewayData"
class="pt-5 my-0"
/> />
</div> </div>
</template> </template>
@@ -56,7 +59,7 @@ import SettingFields from '@/components/Settings/SettingFields.vue'
const show = defineModel<boolean>({ required: true, default: false }) const show = defineModel<boolean>({ required: true, default: false })
const paymentGateways = defineModel<any>('paymentGateways') const paymentGateways = defineModel<any>('paymentGateways')
const newGateway = ref(null) const newGateway = ref(null)
const newGatewayFields = ref([]) const newGatewayFields = ref<{ columns: { fields: any[] }[] }[]>([])
const newGatewayData = ref<Record<string, any>>({}) const newGatewayData = ref<Record<string, any>>({})
const props = defineProps<{ const props = defineProps<{
@@ -72,6 +75,7 @@ const paymentGateway = createResource({
}, },
transform(data: any) { transform(data: any) {
arrangeFields(data.fields) arrangeFields(data.fields)
data.sections = makeSections(data.fields)
return data return data
}, },
}) })
@@ -102,10 +106,6 @@ const arrangeFields = (fields: any[]) => {
} }
return 0 return 0
}) })
fields.splice(3, 0, {
type: 'Column Break',
})
} }
watch( watch(
@@ -130,7 +130,7 @@ watch(newGateway, () => {
gatewayFields.reload({ doctype: gatewayDoc.name }).then(() => { gatewayFields.reload({ doctype: gatewayDoc.name }).then(() => {
let fields = gatewayFields.data || [] let fields = gatewayFields.data || []
arrangeFields(fields) arrangeFields(fields)
newGatewayFields.value = fields newGatewayFields.value = makeSections(fields)
prepareGatewayData() prepareGatewayData()
}) })
}) })
@@ -192,19 +192,6 @@ const getGatewayFields = () => {
}, {}) }, {})
} }
const createGatewayRecord = (gatewayDoc: any, data: any = {}) => {
call('frappe.client.insert', {
doc: {
doctype: 'Payment Gateway',
gateway: newGateway.value,
gateway_controller: gatewayDoc.issingle ? '' : gatewayDoc.name,
gateway_settings: gatewayDoc.issingle ? '' : data.name,
},
}).then(() => {
paymentGateways.value?.reload()
})
}
const allGatewayOptions = computed(() => { const allGatewayOptions = computed(() => {
let options: string[] = [] let options: string[] = []
let gatewayList = allGateways.data?.map((gateway: any) => gateway.name) || [] let gatewayList = allGateways.data?.map((gateway: any) => gateway.name) || []
@@ -230,4 +217,20 @@ const prepareGatewayData = () => {
}) })
} }
} }
const makeSections = (fields: any[]) => {
const columnCount = fields.length / 3
let sections: { columns: { fields: any[] }[] }[] = [
{
columns: [],
},
]
for (let i = 0; i < columnCount; i++) {
sections[0].columns.push({
fields: fields.slice(i * 3, i * 3 + 3),
})
}
return sections
}
</script> </script>
+2 -2
View File
@@ -7,8 +7,8 @@
:size="size" :size="size"
v-bind="$attrs" v-bind="$attrs"
> >
<template v-if="user.open_to === 'Opportunities'" #indicator> <template v-if="user.open_to === 'Work'" #indicator>
<Tooltip :text="__('Open to Opportunities')" placement="right"> <Tooltip :text="__('Open to Work')" placement="right">
<div class="rounded-full bg-surface-green-3 w-fit"> <div class="rounded-full bg-surface-green-3 w-fit">
<BadgeCheckIcon :class="'text-ink-white ' + checkSize" /> <BadgeCheckIcon :class="'text-ink-white ' + checkSize" />
</div> </div>
-1
View File
@@ -1,3 +1,2 @@
@import './assets/Inter/inter.css';
@import 'frappe-ui/style.css'; @import 'frappe-ui/style.css';
@import './styles/codemirror.css'; @import './styles/codemirror.css';
+7 -2
View File
@@ -1,5 +1,5 @@
import './index.css' import './index.css'
import { createApp } from 'vue' import { createApp, watch } from 'vue'
import router from './router' import router from './router'
import App from './App.vue' import App from './App.vue'
import { createPinia } from 'pinia' import { createPinia } from 'pinia'
@@ -19,7 +19,6 @@ app.use(FrappeUI)
app.use(pinia) app.use(pinia)
app.use(router) app.use(router)
app.use(translationPlugin) app.use(translationPlugin)
app.use(telemetryPlugin, { app_name: 'lms' })
app.use(pageMetaPlugin) app.use(pageMetaPlugin)
app.provide('$dayjs', dayjs) app.provide('$dayjs', dayjs)
app.provide('$socket', initSocket()) app.provide('$socket', initSocket())
@@ -29,5 +28,11 @@ const { userResource, allUsers } = usersStore()
app.provide('$user', userResource) app.provide('$user', userResource)
app.provide('$allUsers', allUsers) app.provide('$allUsers', allUsers)
watch(userResource, () => {
if (userResource.data) {
app.use(telemetryPlugin, { app_name: 'lms' })
}
})
app.config.globalProperties.$user = userResource app.config.globalProperties.$user = userResource
app.config.globalProperties.$dialog = createDialog app.config.globalProperties.$dialog = createDialog
+7 -7
View File
@@ -42,8 +42,8 @@
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<FormControl <FormControl
v-model="openToOpportunities" v-model="openToWork"
:label="__('Open to Opportunities')" :label="__('Open to Work')"
type="checkbox" type="checkbox"
@change="updateParticipants()" @change="updateParticipants()"
/> />
@@ -140,7 +140,7 @@ import UserAvatar from '@/components/UserAvatar.vue'
const filters = ref({}) const filters = ref({})
const currentCategory = ref('') const currentCategory = ref('')
const nameFilter = ref('') const nameFilter = ref('')
const openToOpportunities = ref(false) const openToWork = ref(false)
const hiring = ref(false) const hiring = ref(false)
const { brand } = sessionStore() const { brand } = sessionStore()
const memberCount = ref(0) const memberCount = ref(0)
@@ -197,8 +197,8 @@ const updateFilters = () => {
...(nameFilter.value && { ...(nameFilter.value && {
member_name: ['like', `%${nameFilter.value}%`], member_name: ['like', `%${nameFilter.value}%`],
}), }),
...(openToOpportunities.value && { ...(openToWork.value && {
open_to_opportunities: true, open_to_work: true,
}), }),
...(hiring.value && { ...(hiring.value && {
hiring: true, hiring: true,
@@ -211,7 +211,7 @@ const setQueryParams = () => {
let filterKeys = { let filterKeys = {
category: currentCategory.value, category: currentCategory.value,
name: nameFilter.value, name: nameFilter.value,
'open-to-opportunities': openToOpportunities.value, 'open-to-work': openToWork.value,
hiring: hiring.value, hiring: hiring.value,
} }
@@ -240,7 +240,7 @@ const setFiltersFromQuery = () => {
let queries = new URLSearchParams(location.search) let queries = new URLSearchParams(location.search)
nameFilter.value = queries.get('name') || '' nameFilter.value = queries.get('name') || ''
currentCategory.value = queries.get('category') || '' currentCategory.value = queries.get('category') || ''
openToOpportunities.value = queries.get('open-to-opportunities') === 'true' openToWork.value = queries.get('open-to-opportunities') === 'true'
hiring.value = queries.get('hiring') === 'true' hiring.value = queries.get('hiring') === 'true'
} }
+10 -10
View File
@@ -51,12 +51,12 @@
class="hidden lg:block" class="hidden lg:block"
@change="updateJobs" @change="updateJobs"
/> />
<div class="grid grid-cols-2 gap-4"> <div class="flex items-center space-x-4">
<FormControl <FormControl
type="text" type="text"
:placeholder="__('Search')" :placeholder="__('Search')"
v-model="searchQuery" v-model="searchQuery"
class="w-full max-w-40" class="w-full"
@input="updateJobs" @input="updateJobs"
> >
<template #prefix> <template #prefix>
@@ -79,17 +79,17 @@
v-model="jobType" v-model="jobType"
type="select" type="select"
:options="jobTypes" :options="jobTypes"
class="w-full" class="w-full min-w-32"
:placeholder="__('Type')" :placeholder="__('Type')"
@change="updateJobs" @update:modelValue="updateJobs"
/> />
<FormControl <FormControl
v-model="workMode" v-model="workMode"
type="select" type="select"
:options="workModes" :options="workModes"
class="w-full" class="w-full min-w-32"
:placeholder="__('Work Mode')" :placeholder="__('Work Mode')"
@change="updateJobs" @update:modelValue="updateJobs"
/> />
</div> </div>
</div> </div>
@@ -218,13 +218,13 @@ const updateJobs = () => {
const updateFilters = () => { const updateFilters = () => {
filters.value.status = 'Open' filters.value.status = 'Open'
if (jobType.value) { if (jobType.value && jobType.value !== ' ') {
filters.value.type = jobType.value filters.value.type = jobType.value
} else { } else {
delete filters.value.type delete filters.value.type
} }
if (workMode.value) { if (workMode.value && workMode.value !== ' ') {
filters.value.work_mode = workMode.value filters.value.work_mode = workMode.value
} else { } else {
delete filters.value.work_mode delete filters.value.work_mode
@@ -271,7 +271,7 @@ watch(jobs, () => {
const jobTypes = computed(() => { const jobTypes = computed(() => {
return [ return [
{ label: '', value: '' }, { label: ' ', value: ' ' },
{ 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' },
@@ -281,7 +281,7 @@ const jobTypes = computed(() => {
const workModes = computed(() => { const workModes = computed(() => {
return [ return [
{ label: '', value: '' }, { label: ' ', value: ' ' },
{ 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' },
+3 -3
View File
@@ -65,8 +65,8 @@
<Tooltip <Tooltip
v-if="profile.data.open_to" v-if="profile.data.open_to"
:text=" :text="
profile.data.open_to === 'Opportunities' profile.data.open_to === 'Work'
? __('Open to Opportunities') ? __('Open to Work')
: __('Hiring') : __('Hiring')
" "
placement="right" placement="right"
@@ -77,7 +77,7 @@
<div <div
class="rounded-full w-fit" class="rounded-full w-fit"
:class=" :class="
profile.data.open_to === 'Opportunities' profile.data.open_to === 'Work'
? 'bg-surface-green-3' ? 'bg-surface-green-3'
: 'bg-purple-500' : 'bg-purple-500'
" "
+24 -14
View File
@@ -116,20 +116,30 @@ const debouncedSaveProgress = (scormDetails) => {
} }
const saveDataToLMS = (key, value) => { const saveDataToLMS = (key, value) => {
if (key === 'cmi.core.lesson_status') { const isLessonStatus = key === 'cmi.core.lesson_status' && value === 'passed'
if (value === 'passed') { const isCompletionStatus =
isSuccessfullyCompleted.value = true key === 'cmi.completion_status' && value === 'completed'
saveProgress({ const shouldRestart =
is_complete: isSuccessfullyCompleted.value, (key === 'cmi.core.lesson_status' && value === 'failed') ||
scorm_content: '', (key === 'cmi.completion_status' && value === 'incomplete')
})
} else if (value === 'failed' && courseRestartOnFailure) { if (isLessonStatus || isCompletionStatus) {
saveProgress({ isSuccessfullyCompleted.value = true
is_complete: isSuccessfullyCompleted.value, }
scorm_content: '',
}) if (
} isLessonStatus ||
} else if (key === 'cmi.suspend_data' && !isSuccessfullyCompleted.value) { isCompletionStatus ||
(shouldRestart && courseRestartOnFailure)
) {
saveProgress({
is_complete: isSuccessfullyCompleted.value,
scorm_content: '',
})
return
}
if (key === 'cmi.suspend_data' && !isSuccessfullyCompleted.value) {
debouncedSaveProgress({ debouncedSaveProgress({
is_complete: false, is_complete: false,
scorm_content: value, scorm_content: value,
+1 -1
View File
@@ -21,5 +21,5 @@ export default {
}, },
}, },
}, },
plugins: [require('@tailwindcss/line-clamp')], plugins: [],
} }
+45 -1
View File
@@ -1,3 +1,5 @@
import json
import frappe import frappe
ALLOWED_PATHS = [ ALLOWED_PATHS = [
@@ -12,6 +14,15 @@ ALLOWED_PATHS = [
"/api/method/frappe.integrations.oauth2.authorize", "/api/method/frappe.integrations.oauth2.authorize",
"/api/method/frappe.integrations.oauth2.approve", "/api/method/frappe.integrations.oauth2.approve",
"/api/method/frappe.integrations.oauth2.get_token", "/api/method/frappe.integrations.oauth2.get_token",
"/api/method/frappe.www.login.login_via_google",
"/api/method/frappe.www.login.login_via_github",
"/api/method/frappe.www.login.login_via_facebook",
"/api/method/frappe.www.login.login_via_frappe",
"/api/method/frappe.www.login.login_via_office365",
"/api/method/frappe.www.login.login_via_salesforce",
"/api/method/frappe.www.login.login_via_fairlogin",
"/api/method/frappe.www.login.login_via_keycloak",
"/api/method/frappe.www.login.custom",
"/api/method/frappe.integrations.oauth2.openid_profile", "/api/method/frappe.integrations.oauth2.openid_profile",
"/api/method/frappe.website.doctype.web_page_view.web_page_view.make_view_log", "/api/method/frappe.website.doctype.web_page_view.web_page_view.make_view_log",
"/api/method/upload_file", "/api/method/upload_file",
@@ -33,10 +44,14 @@ ALLOWED_PATHS = [
"/api/method/frappe.utils.print_format.download_pdf", "/api/method/frappe.utils.print_format.download_pdf",
"/api/method/frappe.desk.search.search_link", "/api/method/frappe.desk.search.search_link",
"/api/method/frappe.core.doctype.communication.email.make", "/api/method/frappe.core.doctype.communication.email.make",
"/api/method/frappe.core.doctype.user.user.reset_password",
] ]
def authenticate(): def authenticate():
if not frappe.conf.get("block_endpoints"):
return
if frappe.form_dict.cmd: if frappe.form_dict.cmd:
path = f"/api/method/{frappe.form_dict.cmd}" path = f"/api/method/{frappe.form_dict.cmd}"
else: else:
@@ -48,10 +63,39 @@ def authenticate():
if not path.startswith("/api/"): if not path.startswith("/api/"):
return return
print("path", path)
if path.startswith("/lms") or path.startswith("/api/method/lms."): if path.startswith("/lms") or path.startswith("/api/method/lms."):
return return
if is_server_script_path(path):
return
if is_custom_app_endpoint(path):
return
if path in ALLOWED_PATHS: if path in ALLOWED_PATHS:
return return
frappe.throw(f"Access not allowed for this URL: {path}", frappe.PermissionError) frappe.throw(f"Access not allowed for this URL: {path}", frappe.PermissionError)
def is_server_script_path(path):
endpoint = path.split("/api/method/")[-1]
if frappe.db.exists("Server Script", {"script_type": "API", "api_method": endpoint, "disabled": 0}):
return True
return False
def is_custom_app_endpoint(path):
allowed_custom_endpoints = frappe.conf.get("allowed_custom_endpoints", [])
if isinstance(allowed_custom_endpoints, str):
try:
parsed = json.loads(allowed_custom_endpoints)
allowed_custom_endpoints = parsed if isinstance(parsed, list) else [allowed_custom_endpoints]
except Exception:
allowed_custom_endpoints = [allowed_custom_endpoints]
for endpoint in allowed_custom_endpoints:
if endpoint in path:
return True
return False
+2 -2
View File
@@ -257,12 +257,12 @@
"length": 0, "length": 0,
"link_filters": null, "link_filters": null,
"mandatory_depends_on": null, "mandatory_depends_on": null,
"modified": "2025-12-24 12:56:32.110405", "modified": "2025-12-24 12:56:32.110406",
"module": null, "module": null,
"name": "User-open_to", "name": "User-open_to",
"no_copy": 0, "no_copy": 0,
"non_negative": 0, "non_negative": 0,
"options": "\nOpportunities\nHiring", "options": "\nWork\nHiring",
"permlevel": 0, "permlevel": 0,
"precision": "", "precision": "",
"print_hide": 0, "print_hide": 0,
+2 -2
View File
@@ -331,8 +331,8 @@ def get_certification_query(filters):
) )
if field == "member_name": if field == "member_name":
query = query.where(Certificate.member_name.like(value[1])) query = query.where(Certificate.member_name.like(value[1]))
if field == "open_to_opportunities": if field == "open_to_work":
query = query.where(User.open_to == "Opportunities") query = query.where(User.open_to == "Work")
if field == "hiring": if field == "hiring":
query = query.where(User.open_to == "Hiring") query = query.where(User.open_to == "Hiring")
return query return query
-8
View File
@@ -3,14 +3,6 @@
frappe.ui.form.on("LMS Batch", { frappe.ui.form.on("LMS Batch", {
onload: function (frm) { onload: function (frm) {
frm.set_query("student", "students", function (doc) {
return {
filters: {
ignore_user_type: 1,
},
};
});
frm.set_query("reference_doctype", "timetable", function () { frm.set_query("reference_doctype", "timetable", function () {
let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"]; let doctypes = ["Course Lesson", "LMS Quiz", "LMS Assignment"];
return { return {
+1 -1
View File
@@ -37,7 +37,7 @@ class LMSBatch(Document):
def on_update(self): def on_update(self):
if self.has_value_changed("published") and self.published: if self.has_value_changed("published") and self.published:
frappe.enqueue(send_notification_for_published_batch, batch=self, now=True) frappe.enqueue(send_notification_for_published_batch, batch=self)
def autoname(self): def autoname(self):
if not self.name: if not self.name:
+7 -7
View File
@@ -166,14 +166,14 @@ class TestLMSUtils(BaseTestUtils):
certified_participants_no_match = get_certified_participants(filters=filters) certified_participants_no_match = get_certified_participants(filters=filters)
self.assertEqual(len(certified_participants_no_match), 0) self.assertEqual(len(certified_participants_no_match), 0)
def test_certified_participants_with_open_to_opportunities(self): def test_certified_participants_with_open_to_work(self):
filters = {"open_to_opportunities": 1} filters = {"open_to_work": 1}
certified_participants_open_to_oppo = get_certified_participants(filters=filters) certified_participants_open_to_work = get_certified_participants(filters=filters)
self.assertEqual(len(certified_participants_open_to_oppo), 0) self.assertEqual(len(certified_participants_open_to_work), 0)
frappe.db.set_value("User", self.student1.email, "open_to", "Opportunities") frappe.db.set_value("User", self.student1.email, "open_to", "Work")
certified_participants_open_to_oppo = get_certified_participants(filters=filters) certified_participants_open_to_work = get_certified_participants(filters=filters)
self.assertEqual(len(certified_participants_open_to_oppo), 1) self.assertEqual(len(certified_participants_open_to_work), 1)
frappe.db.set_value("User", self.student1.email, "open_to", "") frappe.db.set_value("User", self.student1.email, "open_to", "")
def test_certified_participants_with_open_to_hiring(self): def test_certified_participants_with_open_to_hiring(self):
+468 -353
View File
File diff suppressed because it is too large Load Diff
+386 -271
View File
File diff suppressed because it is too large Load Diff
+372 -257
View File
File diff suppressed because it is too large Load Diff
+372 -257
View File
File diff suppressed because it is too large Load Diff
+380 -265
View File
File diff suppressed because it is too large Load Diff
+372 -257
View File
File diff suppressed because it is too large Load Diff
+395 -280
View File
File diff suppressed because it is too large Load Diff
+394 -279
View File
File diff suppressed because it is too large Load Diff
+392 -277
View File
File diff suppressed because it is too large Load Diff
+375 -260
View File
File diff suppressed because it is too large Load Diff
+379 -264
View File
File diff suppressed because it is too large Load Diff
+372 -257
View File
File diff suppressed because it is too large Load Diff
+385 -270
View File
File diff suppressed because it is too large Load Diff
+372 -257
View File
File diff suppressed because it is too large Load Diff
+400 -285
View File
File diff suppressed because it is too large Load Diff
+375 -260
View File
File diff suppressed because it is too large Load Diff
+388 -273
View File
File diff suppressed because it is too large Load Diff
+372 -257
View File
File diff suppressed because it is too large Load Diff
+390 -275
View File
File diff suppressed because it is too large Load Diff
+378 -263
View File
File diff suppressed because it is too large Load Diff
+372 -257
View File
File diff suppressed because it is too large Load Diff
+373 -258
View File
File diff suppressed because it is too large Load Diff
+373 -258
View File
File diff suppressed because it is too large Load Diff
+377 -262
View File
File diff suppressed because it is too large Load Diff
+372 -257
View File
File diff suppressed because it is too large Load Diff
+372 -257
View File
File diff suppressed because it is too large Load Diff
+397 -282
View File
File diff suppressed because it is too large Load Diff
+375 -260
View File
File diff suppressed because it is too large Load Diff
+378 -263
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -114,4 +114,5 @@ lms.patches.v2_0.count_in_program
lms.patches.v2_0.fix_scorm_lesson_reference_idx #02-09-2025 lms.patches.v2_0.fix_scorm_lesson_reference_idx #02-09-2025
lms.patches.v2_0.certified_members_to_certifications #05-10-2025 lms.patches.v2_0.certified_members_to_certifications #05-10-2025
lms.patches.v2_0.fix_job_application_resume_urls 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
+12
View File
@@ -0,0 +1,12 @@
import frappe
def execute():
open_to_field_exists = frappe.db.exists("Custom Field", {"dt": "User", "fieldname": "open_to"})
if not open_to_field_exists:
return
open_to_opportunities = frappe.get_all("User", {"open_to": "Opportunities"}, ["name"])
for user in open_to_opportunities:
frappe.db.set_value("User", user.name, "open_to", "Work")
+8 -16
View File
@@ -11,24 +11,16 @@ class TestAuth(FrappeAPITestCase, BaseTestUtils):
self.normal_user = self._create_user("normal-user@example.com", "Normal", "User", ["LMS Student"]) self.normal_user = self._create_user("normal-user@example.com", "Normal", "User", ["LMS Student"])
def test_allowed_path(self): def test_allowed_path(self):
site_url = frappe.utils.get_site_url(frappe.local.site) frappe.form_dict.cmd = "ping"
headers = {"Authorization": "Bearer set_test_example_user"} frappe.session.user = self.normal_user.name
url = site_url + "/api/method/lms.lms.utils.get_courses" authenticate()
response = self.get( frappe.session.user = "Administrator"
url,
headers=headers,
)
self.assertNotEqual(response.json.get("exc_type"), "PermissionError")
def test_not_allowed_path(self): def test_not_allowed_path(self):
site_url = frappe.utils.get_site_url(frappe.local.site) frappe.form_dict.cmd = "frappe.auth.get_logged_user"
headers = {"Authorization": "Bearer set_test_example_user"} frappe.session.user = self.normal_user.name
url = site_url + "/api/method/frappe.auth.get_logged_user" self.assertRaises(frappe.PermissionError, authenticate)
response = self.get( frappe.session.user = "Administrator"
url,
headers=headers,
)
self.assertEqual(response.json.get("exc_type"), "PermissionError")
def tearDown(self): def tearDown(self):
BaseTestUtils.tearDown(self) BaseTestUtils.tearDown(self)