Merge remote-tracking branch 'upstream/develop' into fix/course-deletion
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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");
|
|
||||||
}
|
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,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';
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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' },
|
||||||
|
|||||||
@@ -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'
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -21,5 +21,5 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [require('@tailwindcss/line-clamp')],
|
plugins: [],
|
||||||
}
|
}
|
||||||
|
|||||||
+45
-1
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
File diff suppressed because it is too large
Load Diff
+386
-271
File diff suppressed because it is too large
Load Diff
+372
-257
File diff suppressed because it is too large
Load Diff
+372
-257
File diff suppressed because it is too large
Load Diff
+380
-265
File diff suppressed because it is too large
Load Diff
+372
-257
File diff suppressed because it is too large
Load Diff
+395
-280
File diff suppressed because it is too large
Load Diff
+394
-279
File diff suppressed because it is too large
Load Diff
+392
-277
File diff suppressed because it is too large
Load Diff
+375
-260
File diff suppressed because it is too large
Load Diff
+379
-264
File diff suppressed because it is too large
Load Diff
+372
-257
File diff suppressed because it is too large
Load Diff
+385
-270
File diff suppressed because it is too large
Load Diff
+372
-257
File diff suppressed because it is too large
Load Diff
+400
-285
File diff suppressed because it is too large
Load Diff
+375
-260
File diff suppressed because it is too large
Load Diff
+388
-273
File diff suppressed because it is too large
Load Diff
+372
-257
File diff suppressed because it is too large
Load Diff
+390
-275
File diff suppressed because it is too large
Load Diff
+378
-263
File diff suppressed because it is too large
Load Diff
+372
-257
File diff suppressed because it is too large
Load Diff
+373
-258
File diff suppressed because it is too large
Load Diff
+373
-258
File diff suppressed because it is too large
Load Diff
+377
-262
File diff suppressed because it is too large
Load Diff
+372
-257
File diff suppressed because it is too large
Load Diff
+372
-257
File diff suppressed because it is too large
Load Diff
+397
-282
File diff suppressed because it is too large
Load Diff
+375
-260
File diff suppressed because it is too large
Load Diff
+378
-263
File diff suppressed because it is too large
Load Diff
+2
-1
@@ -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
|
||||||
@@ -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
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user