Merge pull request #1957 from frappe/develop

chore: merge 'develop' into 'main'
This commit is contained in:
Jannat Patel
2025-12-31 12:19:21 +05:30
committed by GitHub
28 changed files with 561 additions and 302 deletions
+2 -2
View File
@@ -34,11 +34,11 @@ jobs:
- name: setup python
uses: actions/setup-python@v2
with:
python-version: '3.10'
python-version: '3.14'
- name: setup node
uses: actions/setup-node@v4
with:
node-version: '20'
node-version: '24'
check-latest: true
- name: setup cache for bench
uses: actions/cache@v4
+2 -2
View File
@@ -38,7 +38,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
python-version: '3.14'
- name: Check for valid Python & Merge Conflicts
run: |
@@ -50,7 +50,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
check-latest: true
- name: Add to Hosts
+11
View File
@@ -80,6 +80,7 @@ onMounted(() => {
{},
{
onSuccess(data) {
destructureSidebarLinks()
filterLinksToShow(data)
addOtherLinks()
},
@@ -103,6 +104,16 @@ watch(showMenu, (val) => {
}
})
const destructureSidebarLinks = () => {
let links = []
sidebarLinks.value.forEach((link) => {
link.items?.forEach((item) => {
links.push(item)
})
})
sidebarLinks.value = links
}
const filterLinksToShow = (data) => {
Object.keys(data).forEach((key) => {
if (!parseInt(data[key])) {
+91 -63
View File
@@ -1,72 +1,70 @@
<template>
<Dialog
:options="{
title: 'Edit your profile',
size: '3xl',
}"
>
<template #body-content>
<div>
<div class="grid grid-cols-2 gap-10">
<div>
<div class="text-xs text-ink-gray-5 mb-1">
{{ __('Profile Image') }}
</div>
<FileUploader
v-if="!profile.image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
>
<div class="mb-4">
<Button @click="openFileSelector" :loading="uploading">
{{
uploading
? `Uploading ${progress}%`
: 'Upload a profile image'
}}
</Button>
</div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img
:src="profile.image?.file_url"
class="object-cover h-[50px] w-[50px] rounded-full border-4 border-white object-cover"
/>
<div class="text-base flex flex-col ml-2">
<span>
{{ profile.image?.file_name }}
</span>
<span class="text-sm text-ink-gray-4 mt-1">
{{ getFileSize(profile.image?.file_size) }}
</span>
</div>
<X
@click="removeImage()"
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
/>
</div>
</div>
</div>
<Switch
v-model="profile.looking_for_job"
:label="__('Open to Opportunities')"
:description="
__('Show recruiters and others that you are open to work.')
"
class="!px-0"
/>
<template #body-header>
<div class="flex items-center mb-5">
<div class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ __('Edit Profile') }}
</div>
<Badge v-if="isDirty" class="ml-4" theme="orange">
{{ __('Not Saved') }}
</Badge>
</div>
</template>
<template #body-content>
<div class="text-base">
<div class="grid grid-cols-2 gap-10">
<div class="space-y-4">
<div class="space-y-4">
<div>
<div class="text-xs text-ink-gray-5 mb-1">
{{ __('Profile Image') }}
</div>
<FileUploader
v-if="!profile.image"
:fileTypes="['image/*']"
:validateFile="validateFile"
@success="(file) => saveImage(file)"
>
<template
v-slot="{ file, progress, uploading, openFileSelector }"
>
<div class="mb-4">
<Button @click="openFileSelector" :loading="uploading">
{{
uploading
? `Uploading ${progress}%`
: 'Upload a profile image'
}}
</Button>
</div>
</template>
</FileUploader>
<div v-else class="mb-4">
<div class="flex items-center">
<img
:src="profile.image?.file_url"
class="object-cover h-[50px] w-[50px] rounded-full border-4 border-white object-cover"
/>
<div class="text-base flex flex-col ml-2">
<span>
{{ profile.image?.file_name }}
</span>
<span class="text-sm text-ink-gray-4 mt-1">
{{ getFileSize(profile.image?.file_size) }}
</span>
</div>
<X
@click="removeImage()"
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
/>
</div>
</div>
</div>
<FormControl
v-model="profile.first_name"
:label="__('First Name')"
@@ -89,6 +87,13 @@
</div>
</div>
<div class="space-y-4">
<FormControl
v-model="profile.open_to"
type="select"
:options="[' ', 'Opportunities', 'Hiring']"
:label="__('Open to')"
:placeholder="__('Looking for new work or hiring talent?')"
/>
<Link
:label="__('Language')"
v-model="profile.language"
@@ -103,7 +108,7 @@
@change="(val) => (profile.bio = val)"
:content="profile.bio"
:rows="15"
editorClass="prose-sm py-2 px-2 min-h-[200px] border-outline-gray-2 hover:border-outline-gray-3 rounded-b-md bg-surface-gray-3"
editorClass="prose-sm py-2 px-2 min-h-[280px] border-outline-gray-2 hover:border-outline-gray-3 rounded-b-md bg-surface-gray-3"
/>
</div>
</div>
@@ -121,12 +126,12 @@
</template>
<script setup>
import {
Badge,
Button,
createResource,
Dialog,
FormControl,
FileUploader,
Switch,
TextEditor,
toast,
} from 'frappe-ui'
@@ -137,6 +142,7 @@ import Link from '@/components/Controls/Link.vue'
const reloadProfile = defineModel('reloadProfile')
const hasLanguageChanged = ref(false)
const isDirty = ref(false)
const props = defineProps({
profile: {
@@ -151,7 +157,7 @@ const profile = reactive({
headline: '',
bio: '',
image: '',
looking_for_job: false,
open_to: '',
linkedin: '',
github: '',
twitter: '',
@@ -222,6 +228,27 @@ const removeImage = () => {
profile.image = null
}
watch(
() => profile,
(newVal) => {
if (!props.profile.data) return
let keys = Object.keys(newVal)
keys.splice(keys.indexOf('image'), 1)
for (let key of keys) {
if (newVal[key] !== props.profile.data[key]) {
isDirty.value = true
return
}
}
if (profile.image?.file_url !== props.profile.data.user_image) {
isDirty.value = true
return
}
isDirty.value = false
},
{ deep: true }
)
watch(
() => props.profile.data,
(newVal) => {
@@ -231,11 +258,12 @@ watch(
profile.headline = newVal.headline
profile.language = newVal.language
profile.bio = newVal.bio
profile.looking_for_job = newVal.looking_for_job
profile.open_to = newVal.open_to
profile.linkedin = newVal.linkedin
profile.github = newVal.github
profile.twitter = newVal.twitter
if (newVal.user_image) imageResource.submit({ image: newVal.user_image })
isDirty.value = false
}
}
)
+34 -26
View File
@@ -1,42 +1,50 @@
<template>
<div class="border rounded-md w-1/3 mx-auto my-32">
<div class="border-b px-5 py-3 font-medium text-ink-gray-9">
<span
class="inline-flex items-center before:bg-surface-red-5 before:w-2 before:h-2 before:rounded-md before:mr-2"
></span>
{{ __('Not Permitted') }}
</div>
<div v-if="user.data" class="px-5 py-3">
<div class="text-ink-gray-7">
{{ __('You do not have permission to access this page.') }}
<div class="bg-surface-white w-full h-full">
<div class="w-fit mx-auto mt-56 text-center p-4">
<div class="text-3xl font-semibold text-ink-gray-5 pb-4 mb-2 border-b">
{{ __('Not Permitted') }}
</div>
<router-link
:to="{
name: 'Courses',
}"
>
<Button variant="solid" class="mt-2">
{{ __('Checkout Courses') }}
<div v-if="user.data" class="px-5 py-3">
<div class="text-ink-gray-5">
{{ __('You do not have permission to access this page.') }}
</div>
<router-link
:to="{
name: 'Courses',
}"
>
<Button variant="solid" class="mt-2 w-full">
{{ __('Checkout Courses') }}
</Button>
</router-link>
</div>
<div class="px-5 py-3">
<div class="text-ink-gray-5">
{{ __('You are not permitted to access this page.') }}
</div>
<Button @click="redirectToLogin()" class="mt-4 w-full" variant="solid">
{{ __('Login') }}
</Button>
</router-link>
</div>
<div class="px-5 py-3">
<div class="text-ink-gray-7">
{{ __('Please login to access this page.') }}
</div>
<Button @click="redirectToLogin()" class="mt-4">
{{ __('Login') }}
</Button>
</div>
</div>
</template>
<script setup>
import { inject } from 'vue'
import { Button } from 'frappe-ui'
import { Button, usePageMeta } from 'frappe-ui'
import { sessionStore } from '../stores/session'
const user = inject('$user')
const { brand } = sessionStore()
const redirectToLogin = () => {
window.location.href = '/login'
}
usePageMeta(() => {
return {
title: __('Not Permitted'),
icon: brand.favicon,
}
})
</script>
+8 -1
View File
@@ -7,13 +7,20 @@
:size="size"
v-bind="$attrs"
>
<template v-if="user.looking_for_job" #indicator>
<template v-if="user.open_to === 'Opportunities'" #indicator>
<Tooltip :text="__('Open to Opportunities')" placement="right">
<div class="rounded-full bg-surface-green-3 w-fit">
<BadgeCheckIcon :class="'text-ink-white ' + checkSize" />
</div>
</Tooltip>
</template>
<template v-else-if="user.open_to === 'Hiring'" #indicator>
<Tooltip :text="__('Hiring')" placement="right">
<div class="rounded-full bg-purple-500 w-fit">
<BadgeCheckIcon :class="'text-ink-white ' + checkSize" />
</div>
</Tooltip>
</template>
</Avatar>
</template>
<script setup>
+56 -10
View File
@@ -91,6 +91,16 @@
</Button>
</div>
</div>
<p
class="bg-surface-amber-2 text-ink-amber-2 text-sm leading-5 p-2 rounded-md"
>
{{
__(
'Please ensure that the billing name you enter is correct, as it will be used on your invoice.'
)
}}
</p>
</div>
<div class="flex-1 lg:mr-10">
@@ -104,16 +114,22 @@
<FormControl
:label="__('Billing Name')"
v-model="billingDetails.billing_name"
:required="true"
/>
<FormControl
:label="__('Address Line 1')"
v-model="billingDetails.address_line1"
:required="true"
/>
<FormControl
:label="__('Address Line 2')"
v-model="billingDetails.address_line2"
/>
<FormControl :label="__('City')" v-model="billingDetails.city" />
<FormControl
:label="__('City')"
v-model="billingDetails.city"
:required="true"
/>
<FormControl
:label="__('State/Province')"
v-model="billingDetails.state"
@@ -125,20 +141,24 @@
:value="billingDetails.country"
@change="(option) => changeCurrency(option)"
:label="__('Country')"
:required="true"
/>
<FormControl
:label="__('Postal Code')"
v-model="billingDetails.pincode"
:required="true"
/>
<FormControl
:label="__('Phone Number')"
v-model="billingDetails.phone"
:required="true"
/>
<Link
doctype="LMS Source"
:value="billingDetails.source"
@change="(option) => (billingDetails.source = option)"
:label="__('Where did you hear about us?')"
:required="true"
/>
<FormControl
v-if="billingDetails.country == 'India'"
@@ -152,14 +172,29 @@
/>
</div>
</div>
<div class="flex items-center justify-between border-t pt-4 mt-8">
<p class="text-ink-gray-5">
{{
__(
'Make sure to enter the correct billing name as the same will be used in your invoice.'
)
}}
</p>
<div
class="flex flex-col lg:flex-row items-start lg:items-center justify-between border-t pt-4 mt-8 space-y-4 lg:space-y-0"
>
<div>
<FormControl
:label="
__(
'I consent to my personal information being stored for invoicing'
)
"
type="checkbox"
class="leading-6"
v-model="billingDetails.member_consent"
/>
<div
v-if="showConsentWarning"
class="mt-1 text-xs text-ink-red-3"
>
{{
__('Please provide your consent to proceed with the payment')
}}
</div>
</div>
<Button variant="solid" size="md" @click="generatePaymentLink()">
{{ __('Proceed to Payment') }}
</Button>
@@ -194,7 +229,7 @@ import {
toast,
call,
} from 'frappe-ui'
import { reactive, inject, onMounted, computed, ref } from 'vue'
import { reactive, inject, onMounted, computed, ref, watch } from 'vue'
import { sessionStore } from '../stores/session'
import Link from '@/components/Controls/Link.vue'
import NotPermitted from '@/components/NotPermitted.vue'
@@ -202,6 +237,7 @@ import { X } from 'lucide-vue-next'
const user = inject('$user')
const { brand } = sessionStore()
const showConsentWarning = ref(false)
onMounted(() => {
const script = document.createElement('script')
@@ -296,6 +332,10 @@ const generatePaymentLink = () => {
if (!billingDetails.source) {
return __('Please let us know where you heard about us from.')
}
if (!billingDetails.member_consent) {
showConsentWarning.value = true
return __('Please provide your consent to proceed with the payment.')
}
return validateAddress()
},
onSuccess(data) {
@@ -406,6 +446,12 @@ const redirectTo = computed(() => {
}
})
watch(billingDetails, () => {
if (billingDetails.member_consent) {
showConsentWarning.value = false
}
})
usePageMeta(() => {
return {
title: __('Billing Details'),
+69 -35
View File
@@ -13,27 +13,45 @@
</router-link>
</header>
<div class="mx-auto w-full max-w-4xl pt-6 pb-10">
<div class="flex flex-col md:flex-row justify-between mb-4 px-3">
<div class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0">
<div class="flex flex-col md:flex-row justify-between mb-8 px-3">
<div class="text-xl font-semibold text-ink-gray-9 mb-4 md:mb-0">
{{ memberCount }} {{ __('certified members') }}
</div>
<div class="grid grid-cols-2 gap-2">
<FormControl
v-model="nameFilter"
:placeholder="__('Search by Name')"
type="text"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
@input="updateParticipants()"
/>
<div
v-if="categories.data?.length"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
>
<Select
v-model="currentCategory"
:options="categories.data"
:placeholder="__('Category')"
@update:modelValue="updateParticipants()"
<div
class="flex flex-col md:flex-row md:items-center space-y-4 md:space-y-0 md:space-x-4"
>
<div class="flex items-center space-x-4">
<FormControl
v-model="nameFilter"
:placeholder="__('Search by Name')"
type="text"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
@input="updateParticipants()"
/>
<div
v-if="categories.data?.length"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
>
<Select
v-model="currentCategory"
:options="categories.data"
:placeholder="__('Category')"
@update:modelValue="updateParticipants()"
/>
</div>
</div>
<div class="flex items-center space-x-4">
<FormControl
v-model="openToOpportunities"
:label="__('Open to Opportunities')"
type="checkbox"
@change="updateParticipants()"
/>
<FormControl
v-model="hiring"
:label="__('Hiring')"
type="checkbox"
@change="updateParticipants()"
/>
</div>
</div>
@@ -42,15 +60,15 @@
<template v-for="(participant, index) in participants.data">
<router-link
:to="{
name: 'ProfileCertificates',
name: 'ProfileAbout',
params: {
username: participant.username,
},
}"
class="flex h-15 rounded-md hover:bg-surface-gray-2 px-3"
class="flex rounded-md hover:bg-surface-gray-2 px-3"
>
<div
class="flex items-center w-full space-x-3 py-2"
class="flex w-full space-x-3 py-2"
:class="{
'border-b': index < participants.data.length - 1,
}"
@@ -118,9 +136,11 @@ import { sessionStore } from '../stores/session'
import EmptyState from '@/components/EmptyState.vue'
import UserAvatar from '@/components/UserAvatar.vue'
const currentCategory = ref('')
const filters = ref({})
const currentCategory = ref('')
const nameFilter = ref('')
const openToOpportunities = ref(false)
const hiring = ref(false)
const { brand } = sessionStore()
const memberCount = ref(0)
const dayjs = inject('$dayjs')
@@ -152,7 +172,7 @@ const categories = createListResource({
cache: ['certification_categories'],
auto: true,
transform(data) {
data.unshift({ label: __(''), value: '' })
data.unshift({ label: __(' '), value: ' ' })
return data
},
})
@@ -169,16 +189,19 @@ const updateParticipants = () => {
}
const updateFilters = () => {
if (currentCategory.value) {
filters.value.category = currentCategory.value
} else {
delete filters.value.category
}
if (nameFilter.value) {
filters.value.member_name = ['like', `%${nameFilter.value}%`]
} else {
delete filters.value.member_name
filters.value = {
...(currentCategory.value && {
category: currentCategory.value,
}),
...(nameFilter.value && {
member_name: ['like', `%${nameFilter.value}%`],
}),
...(openToOpportunities.value && {
open_to_opportunities: true,
}),
...(hiring.value && {
hiring: true,
}),
}
}
@@ -187,10 +210,12 @@ const setQueryParams = () => {
let filterKeys = {
category: currentCategory.value,
name: nameFilter.value,
'open-to-opportunities': openToOpportunities.value,
hiring: hiring.value,
}
Object.keys(filterKeys).forEach((key) => {
if (filterKeys[key]) {
if (filterKeys[key] && hasValue(filterKeys[key])) {
queries.set(key, filterKeys[key])
} else {
queries.delete(key)
@@ -203,10 +228,19 @@ const setQueryParams = () => {
)
}
const hasValue = (value) => {
if (typeof value === 'string') {
return value.trim() !== ''
}
return true
}
const setFiltersFromQuery = () => {
let queries = new URLSearchParams(location.search)
nameFilter.value = queries.get('name') || ''
currentCategory.value = queries.get('category') || ''
openToOpportunities.value = queries.get('open-to-opportunities') === 'true'
hiring.value = queries.get('hiring') === 'true'
}
const breadcrumbs = computed(() => [
+57 -41
View File
@@ -26,56 +26,72 @@
</header>
<div>
<div
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between w-full md:w-4/5 mx-auto p-5"
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between w-full md:w-4/5 mx-auto mb-2 p-5"
>
<div class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0">
{{ __('{0} Open Jobs').format(jobCount) }}
</div>
<div class="flex items-center justify-between space-x-4">
<div class="flex items-center justify-between">
<div class="text-xl font-semibold text-ink-gray-9 md:mb-0">
{{ __('{0} {1} Jobs').format(jobCount, activeTab) }}
</div>
<TabButtons
v-if="tabs.length > 1"
v-model="activeTab"
:buttons="tabs"
class="lg:hidden"
@change="updateJobs"
/>
<FormControl
type="text"
:placeholder="__('Search')"
v-model="searchQuery"
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
@input="updateJobs"
>
<template #prefix>
<Search
class="w-4 h-4 stroke-1.5 text-ink-gray-5"
name="search"
/>
</template>
</FormControl>
<Link
v-if="user.data"
doctype="Country"
v-model="country"
:placeholder="__('Country')"
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
/>
<FormControl
v-model="jobType"
type="select"
:options="jobTypes"
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
:placeholder="__('Type')"
@change="updateJobs"
/>
<FormControl
v-model="workMode"
type="select"
:options="workModes"
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
:placeholder="__('Work Mode')"
</div>
<div
class="flex flex-col md:flex-row md:items-center md:space-x-4 space-y-4 md:space-y-0"
>
<TabButtons
v-if="tabs.length > 1"
v-model="activeTab"
:buttons="tabs"
class="hidden lg:block"
@change="updateJobs"
/>
<div class="grid grid-cols-2 gap-4">
<FormControl
type="text"
:placeholder="__('Search')"
v-model="searchQuery"
class="w-full max-w-40"
@input="updateJobs"
>
<template #prefix>
<Search
class="w-4 h-4 stroke-1.5 text-ink-gray-5"
name="search"
/>
</template>
</FormControl>
<Link
v-if="user.data"
doctype="Country"
v-model="country"
:placeholder="__('Country')"
class="w-full"
/>
</div>
<div class="grid grid-cols-2 gap-4">
<FormControl
v-model="jobType"
type="select"
:options="jobTypes"
class="w-full"
:placeholder="__('Type')"
@change="updateJobs"
/>
<FormControl
v-model="workMode"
type="select"
:options="workModes"
class="w-full"
:placeholder="__('Work Mode')"
@change="updateJobs"
/>
</div>
</div>
</div>
<div v-if="jobs.data?.length" class="w-full md:w-4/5 mx-auto p-5 pt-0">
+20 -3
View File
@@ -56,15 +56,32 @@
:src="profile.data.user_image"
class="object-cover h-[100px] w-[100px] rounded-full border-4 border-white object-cover"
/>
<div
v-else
class="flex items-center justify-center h-[100px] w-[100px] rounded-full border-4 border-white bg-surface-gray-2 text-3xl font-semibold text-ink-gray-7"
>
{{ profile.data.full_name.charAt(0).toUpperCase() }}
</div>
<Tooltip
v-if="profile.data.looking_for_job"
:text="__('Open to Opportunities')"
v-if="profile.data.open_to"
:text="
profile.data.open_to === 'Opportunities'
? __('Open to Opportunities')
: __('Hiring')
"
placement="right"
>
<div
class="absolute bottom-3 right-1 p-0.5 bg-surface-white rounded-full"
>
<div class="rounded-full bg-surface-green-3 w-fit">
<div
class="rounded-full w-fit"
:class="
profile.data.open_to === 'Opportunities'
? 'bg-surface-green-3'
: 'bg-purple-500'
"
>
<BadgeCheckIcon class="text-ink-white size-5" />
</div>
</div>
+1 -1
View File
@@ -1 +1 @@
__version__ = "2.43.0"
__version__ = "2.44.0"
+6 -6
View File
@@ -238,8 +238,8 @@
"dt": "User",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "looking_for_job",
"fieldtype": "Check",
"fieldname": "open_to",
"fieldtype": "Select",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
@@ -253,16 +253,16 @@
"insert_after": "verify_terms",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Open to Opportunities",
"label": "Open to",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2021-12-31 12:56:32.110405",
"modified": "2025-12-24 12:56:32.110405",
"module": null,
"name": "User-looking_for_job",
"name": "User-open_to",
"no_copy": 0,
"non_negative": 0,
"options": null,
"options": "\nOpportunities\nHiring",
"permlevel": 0,
"precision": "",
"print_hide": 0,
+1 -1
View File
@@ -136,7 +136,7 @@ def delete_custom_fields():
"medium",
"linkedin",
"profession",
"looking_for_job",
"open_to",
"cover_image" "work_environment",
"dream_companies",
"career_preference_column",
+57 -22
View File
@@ -281,10 +281,34 @@ def get_evaluator_details(evaluator):
@frappe.whitelist(allow_guest=True)
def get_certified_participants(filters=None, start=0, page_length=100):
filters, or_filters, open_to_opportunities, hiring = update_certification_filters(filters)
participants = frappe.db.get_all(
"LMS Certificate",
filters=filters,
or_filters=or_filters,
fields=["member", "issue_date", "batch_name", "course", "name"],
group_by="member",
order_by="issue_date desc",
start=start,
page_length=page_length,
)
for participant in participants:
details = get_certified_participant_details(participant.member)
participant.update(details)
participants = filter_by_open_to_criteria(participants, open_to_opportunities, hiring)
return participants
def update_certification_filters(filters):
open_to_opportunities = False
hiring = False
or_filters = {}
if not filters:
filters = {}
filters.update({"published": 1})
category = filters.get("category")
@@ -293,27 +317,38 @@ def get_certified_participants(filters=None, start=0, page_length=100):
or_filters["course_title"] = ["like", f"%{category}%"]
or_filters["batch_title"] = ["like", f"%{category}%"]
participants = frappe.db.get_all(
"LMS Certificate",
filters=filters,
or_filters=or_filters,
fields=["member", "issue_date"],
group_by="member",
order_by="issue_date desc",
start=start,
page_length=page_length,
)
if filters.get("open_to_opportunities"):
del filters["open_to_opportunities"]
open_to_opportunities = True
for participant in participants:
count = frappe.db.count("LMS Certificate", {"member": participant.member})
details = frappe.db.get_value(
"User",
participant.member,
["full_name", "user_image", "username", "country", "headline", "looking_for_job"],
as_dict=1,
)
details["certificate_count"] = count
participant.update(details)
if filters.get("hiring"):
del filters["hiring"]
hiring = True
return filters, or_filters, open_to_opportunities, hiring
def get_certified_participant_details(member):
count = frappe.db.count("LMS Certificate", {"member": member})
details = frappe.db.get_value(
"User",
member,
["full_name", "user_image", "username", "country", "headline", "open_to"],
as_dict=1,
)
details["certificate_count"] = count
return details
def filter_by_open_to_criteria(participants, open_to_opportunities, hiring):
if not open_to_opportunities and not hiring:
return participants
if open_to_opportunities:
participants = [participant for participant in participants if participant.open_to == "Opportunities"]
if hiring:
participants = [participant for participant in participants if participant.open_to == "Hiring"]
return participants
@@ -1635,7 +1670,7 @@ def get_profile_details(username):
"headline",
"language",
"cover_image",
"looking_for_job",
"open_to",
"linkedin",
"github",
"twitter",
@@ -52,8 +52,14 @@ class TestCourseEvaluator(UnitTestCase):
return first_date
def calculated_last_date_of_schedule(self, first_date):
last_date = add_days(first_date, 56) # 8 weeks course
return last_date
last_day = add_days(first_date, 56)
offset_monday = (0 - last_day.weekday() + 7) % 7 # 0 for Monday
offset_wednesday = (2 - last_day.weekday() + 7) % 7 # 2 for Wednesday
if offset_monday > offset_wednesday and offset_monday < 4:
last_day = add_days(last_day, offset_monday)
else:
last_day = add_days(last_day, offset_wednesday)
return last_day
def test_unavailability_dates(self):
unavailable_from = getdate(self.evaluator.unavailable_from)
+8 -1
View File
@@ -15,6 +15,7 @@
"payment_for_document",
"payment_received",
"payment_for_certificate",
"member_consent",
"payment_details_section",
"original_amount",
"discount_amount",
@@ -181,6 +182,12 @@
"fieldtype": "Currency",
"label": "Original Amount",
"options": "currency"
},
{
"default": "0",
"fieldname": "member_consent",
"fieldtype": "Check",
"label": "Member Consent"
}
],
"index_web_pages_for_search": 1,
@@ -194,7 +201,7 @@
"link_fieldname": "payment"
}
],
"modified": "2025-11-12 12:39:52.466297",
"modified": "2025-12-19 17:55:25.968384",
"modified_by": "sayali@frappe.io",
"module": "LMS",
"name": "LMS Payment",
+1
View File
@@ -121,6 +121,7 @@ def record_payment(
"payment_for_document_type": doctype,
"payment_for_document": docname,
"payment_for_certificate": payment_for_certificate,
"member_consent": address.member_consent,
}
)
if coupon_code:
+32
View File
@@ -2,6 +2,7 @@ import frappe
from frappe.tests import UnitTestCase
from frappe.utils import add_days, nowdate
from lms.lms.api import get_certified_participants
from lms.lms.doctype.lms_certificate.lms_certificate import get_default_certificate_template, is_certified
from .utils import (
@@ -147,6 +148,7 @@ class TestUtils(UnitTestCase):
certificate.member = member
certificate.issue_date = frappe.utils.nowdate()
certificate.template = get_default_certificate_template()
certificate.published = 1
certificate.save()
return certificate
@@ -265,6 +267,36 @@ class TestUtils(UnitTestCase):
self.assertIsNone(is_certified(self.course.name))
frappe.session.user = "Administrator"
def test_certified_participants_with_category(self):
filters = {"category": "Utility Course"}
certified_participants = get_certified_participants(filters=filters)
self.assertEqual(len(certified_participants), 1)
self.assertEqual(certified_participants[0].member, self.student1.email)
filters = {"category": "Nonexistent Category"}
certified_participants_no_match = get_certified_participants(filters=filters)
self.assertEqual(len(certified_participants_no_match), 0)
def test_certified_participants_with_open_to_opportunities(self):
filters = {"open_to_opportunities": 1}
certified_participants_open_to_oppo = get_certified_participants(filters=filters)
self.assertEqual(len(certified_participants_open_to_oppo), 0)
frappe.db.set_value("User", self.student1.email, "open_to", "Opportunities")
certified_participants_open_to_oppo = get_certified_participants(filters=filters)
self.assertEqual(len(certified_participants_open_to_oppo), 1)
frappe.db.set_value("User", self.student1.email, "open_to", "")
def test_certified_participants_with_open_to_hiring(self):
filters = {"hiring": 1}
certified_participants_hiring = get_certified_participants(filters=filters)
self.assertEqual(len(certified_participants_hiring), 0)
frappe.db.set_value("User", self.student1.email, "open_to", "Hiring")
certified_participants_hiring = get_certified_participants(filters=filters)
self.assertEqual(len(certified_participants_hiring), 1)
frappe.db.set_value("User", self.student1.email, "open_to", "")
def test_rating_validation(self):
student3 = self.create_user("student3@example.com", "Emily", "Cooper", ["LMS Student"])
with self.assertRaises(frappe.exceptions.ValidationError):
+44 -44
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2025-12-19 16:05+0000\n"
"PO-Revision-Date: 2025-12-23 23:25\n"
"PO-Revision-Date: 2025-12-29 11:09\n"
"Last-Translator: jannat@frappe.io\n"
"Language-Team: Persian\n"
"MIME-Version: 1.0\n"
@@ -24,11 +24,11 @@ msgstr " لطفا آن را ارزیابی و نمره دهید."
#: frontend/src/pages/Programs/ProgramEnrollment.vue:32
msgid " designed as a learning path to guide your progress. You may take the courses in any order that suits you. "
msgstr ""
msgstr " به عنوان یک مسیر یادگیری برای هدایت پیشرفت شما طراحی شده است. شما می‌توانید دوره‌ها را به هر ترتیبی که برای شما مناسب است، بگذرانید. "
#: frontend/src/pages/Programs/ProgramEnrollment.vue:25
msgid " designed as a structured learning path to guide your progress. Courses in this program must be taken in order, and each course will unlock as you complete the previous one. "
msgstr ""
msgstr " به عنوان یک مسیر یادگیری ساختاریافته برای هدایت پیشرفت شما طراحی شده است. دوره‌های این برنامه باید به ترتیب گذرانده شوند و هر دوره با تکمیل دوره قبلی، قفل آن باز می‌شود. "
#: frontend/src/pages/Home/Streak.vue:21
msgid " you are on a"
@@ -37,7 +37,7 @@ msgstr " شما در حال حاضر در یک"
#. Paragraph text in the LMS Workspace
#: lms/lms/workspace/lms/lms.json
msgid "<a href=\"/app/lms-settings/LMS%20Settings\">LMS Settings</a>"
msgstr ""
msgstr "<a href=\"/app/lms-settings/LMS%20Settings\">تنظیمات LMS</a>"
#. Paragraph text in the LMS Workspace
#: lms/lms/workspace/lms/lms.json
@@ -52,12 +52,12 @@ msgstr ""
#. Paragraph text in the LMS Workspace
#: lms/lms/workspace/lms/lms.json
msgid "<a href=\"/lms/courses/new/edit\">Create a Course</a>"
msgstr ""
msgstr "<a href=\"/lms/courses/new/edit\">ایجاد یک دوره</a>"
#. Paragraph text in the LMS Workspace
#: lms/lms/workspace/lms/lms.json
msgid "<a href=\"https://docs.frappe.io/learning\">Documentation</a>"
msgstr ""
msgstr "<a href=\"https://docs.frappe.io/learning\">مستندات</a>"
#: frontend/src/components/Modals/EmailTemplateModal.vue:50
msgid "<p>Dear {{ member_name }},</p>\\n\\n<p>You have been enrolled in our upcoming batch {{ batch_name }}.</p>\\n\\n<p>Thanks,</p>\\n<p>Frappe Learning</p>"
@@ -124,7 +124,7 @@ msgstr "دستاوردها"
#: frontend/src/pages/Statistics.vue:16
msgid "Active Members"
msgstr ""
msgstr "اعضای فعال"
#: frontend/src/components/Assessments.vue:11
#: frontend/src/components/BatchCourses.vue:11
@@ -148,7 +148,7 @@ msgstr "افزودن فصل"
#: frontend/src/pages/Programs/ProgramForm.vue:176
msgid "Add Course to Program"
msgstr ""
msgstr "افزودن دوره به برنامه"
#: frontend/src/components/Settings/Evaluators.vue:91
msgid "Add Evaluator"
@@ -189,7 +189,7 @@ msgstr "افزودن دانش‌آموز"
#: frontend/src/components/Sidebar/AppSidebar.vue:523
msgid "Add a chapter"
msgstr ""
msgstr "افزودن یک فصل"
#: frontend/src/components/Modals/BatchCourseModal.vue:5
msgid "Add a course"
@@ -201,16 +201,16 @@ msgstr "یک کلمه کلیدی اضافه کنید و سپس اینتر را
#: frontend/src/components/Sidebar/AppSidebar.vue:524
msgid "Add a lesson"
msgstr ""
msgstr "افزودن درس"
#: frontend/src/components/Settings/Members.vue:88
msgid "Add a new member"
msgstr ""
msgstr "افزودن یک عضو جدید"
#: frontend/src/components/Modals/Question.vue:167
#: frontend/src/pages/QuizForm.vue:200
msgid "Add a new question"
msgstr ""
msgstr "افزودن سؤال جدید"
#: frontend/src/components/Sidebar/AppSidebar.vue:538
msgid "Add a program"
@@ -308,7 +308,7 @@ msgstr "همه دوره ها"
#: frontend/src/pages/Programs/StudentPrograms.vue:5
msgid "All Programs"
msgstr ""
msgstr "همه برنامه‌ها"
#: lms/lms/doctype/lms_quiz/lms_quiz.py:42
msgid "All questions should have the same marks if the limit is set."
@@ -419,7 +419,7 @@ msgstr ""
#: frontend/src/components/Settings/Coupons/CouponDetails.vue:71
msgid "Applicable For"
msgstr ""
msgstr "قابل استفاده برای"
#. Label of the applicable_items (Table) field in DocType 'LMS Coupon'
#: lms/lms/doctype/lms_coupon/lms_coupon.json
@@ -535,7 +535,7 @@ msgstr "ارزیابی ها"
#: lms/lms/doctype/lms_badge/lms_badge.js:48
msgid "Assign"
msgstr "اختصاص دهید"
msgstr "اختصاص دادن"
#: frontend/src/components/Settings/BadgeForm.vue:28
msgid "Assign For"
@@ -1676,7 +1676,7 @@ msgstr "طرح کلی دوره"
#: frontend/src/components/Modals/CourseProgressSummary.vue:5
#: lms/lms/report/course_progress_summary/course_progress_summary.json
msgid "Course Progress Summary"
msgstr ""
msgstr "خلاصه پیشرفت دوره"
#. Label of the section_break_7 (Section Break) field in DocType 'LMS Course'
#: lms/lms/doctype/lms_course/lms_course.json
@@ -1811,7 +1811,7 @@ msgstr ""
#: frontend/src/pages/Quizzes.vue:101
msgid "Create a Quiz"
msgstr ""
msgstr "ایجاد یک آزمون"
#: frontend/src/components/Sidebar/AppSidebar.vue:531
msgid "Create a batch"
@@ -2014,7 +2014,7 @@ msgstr "این درس حذف شود؟"
#: frontend/src/pages/CourseForm.vue:617
msgid "Deleting the course will also delete all its chapters and lessons. Are you sure you want to delete this course?"
msgstr ""
msgstr "حذف دوره، تمام فصل‌ها و درس‌های آن را نیز حذف خواهد کرد. آیا از حذف این دوره مطمئن هستید؟"
#: frontend/src/pages/BatchForm.vue:577
msgid "Deleting this batch will also delete all its data including enrolled students, linked courses, assessments, feedback and discussions. Are you sure you want to continue?"
@@ -2887,7 +2887,7 @@ msgstr ""
#: frontend/src/components/Modals/EditProfile.vue:84
msgid "GitHub ID"
msgstr ""
msgstr "شناسه گیت‌هاب"
#. Label of the github (Data) field in DocType 'User'
#: lms/fixtures/custom_field.json
@@ -2944,7 +2944,7 @@ msgstr "سبز"
#: lms/templates/signup-form.html:56
msgid "Have an account? Login"
msgstr "حساب کاربری دارید؟ وارد شدن"
msgstr "حساب کاربری دارید؟ ورود"
#. Label of the headline (Data) field in DocType 'User'
#: frontend/src/components/Modals/EditProfile.vue:78
@@ -3225,7 +3225,7 @@ msgstr ""
#: lms/lms/doctype/course_lesson/course_lesson.py:23
msgid "Invalid Quiz ID"
msgstr ""
msgstr "شناسه آزمون نامعتبر"
#: lms/lms/doctype/course_lesson/course_lesson.py:37
msgid "Invalid Quiz ID in content"
@@ -3859,7 +3859,7 @@ msgstr ""
#: frontend/src/components/QuizBlock.vue:9 frontend/src/pages/Batch.vue:213
#: frontend/src/pages/Lesson.vue:103
msgid "Login"
msgstr "وارد شدن"
msgstr "ورود"
#: frontend/src/components/Sidebar/UserDropdown.vue:190
msgid "Login to Frappe Cloud?"
@@ -4370,7 +4370,7 @@ msgstr ""
#: frontend/src/pages/QuizForm.vue:406 frontend/src/pages/QuizForm.vue:414
msgid "New Quiz"
msgstr ""
msgstr "آزمون جدید"
#: lms/www/new-sign-up.html:3
msgid "New Sign Up"
@@ -4416,7 +4416,7 @@ msgstr ""
#: frontend/src/pages/Quizzes.vue:19
msgid "No Quizzes"
msgstr ""
msgstr "بدون آزمون"
#. Option for the 'Auto Recording' (Select) field in DocType 'LMS Live Class'
#: lms/lms/doctype/lms_live_class/lms_live_class.json
@@ -5281,7 +5281,7 @@ msgstr "تصویر نمایه"
#: frontend/src/pages/Programs/Programs.vue:18
msgid "Program"
msgstr ""
msgstr "برنامه"
#. Label of the program_courses (Table) field in DocType 'LMS Program'
#: lms/lms/doctype/lms_program/lms_program.json
@@ -5373,7 +5373,7 @@ msgstr ""
#: frontend/src/components/CourseCardOverlay.vue:99
#: frontend/src/pages/Programs/ProgramForm.vue:125
msgid "Progress Summary"
msgstr ""
msgstr "خلاصه پیشرفت"
#: frontend/src/pages/Programs/ProgramProgressSummary.vue:5
msgid "Progress Summary for {0}"
@@ -5518,12 +5518,12 @@ msgstr ""
#: frontend/src/components/Quiz.vue:251
msgid "Quiz Summary"
msgstr ""
msgstr "خلاصه آزمون"
#. Label of the quiz_title (Data) field in DocType 'LMS Quiz Submission'
#: lms/lms/doctype/lms_quiz_submission/lms_quiz_submission.json
msgid "Quiz Title"
msgstr ""
msgstr "عنوان آزمون"
#: frontend/src/pages/Quizzes.vue:211
msgid "Quiz created successfully"
@@ -5546,7 +5546,7 @@ msgstr "تکلیف زیر درس نشان داده می‌شود."
#: frontend/src/pages/QuizForm.vue:398 frontend/src/pages/Quizzes.vue:285
#: frontend/src/pages/Quizzes.vue:295 lms/www/lms.py:250
msgid "Quizzes"
msgstr ""
msgstr "آزمون‌ها"
#: frontend/src/pages/Quizzes.vue:233
msgid "Quizzes deleted successfully"
@@ -5745,7 +5745,7 @@ msgstr ""
#: lms/lms/doctype/lms_quiz/lms_quiz.py:33
msgid "Rows {0} have the duplicate questions."
msgstr ""
msgstr "ردیف‌های {0} سوالات تکراری دارند."
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue:56
msgid "Run"
@@ -5930,7 +5930,7 @@ msgstr ""
#: frontend/src/components/AssessmentPlugin.vue:28
msgid "Select a quiz"
msgstr ""
msgstr "انتخاب یک آزمون"
#: frontend/src/components/AssessmentPlugin.vue:35
msgid "Select an assignment"
@@ -6192,7 +6192,7 @@ msgstr ""
#: frontend/src/components/Quiz.vue:81
msgid "Start the Quiz"
msgstr ""
msgstr "شروع آزمون"
#. Option for the 'Company Type' (Select) field in DocType 'User'
#: lms/fixtures/custom_field.json
@@ -6326,7 +6326,7 @@ msgstr "ارسال و ادامه"
#: frontend/src/components/Modals/JobApplicationModal.vue:23
msgid "Submit your resume to proceed with your application for this position. Upon submission, it will be shared with the job poster."
msgstr ""
msgstr "برای ادامه درخواست برای این موقعیت شغلی، رزومه خود را ارسال کنید. پس از ارسال، رزومه با آگهی‌دهنده به اشتراک گذاشته خواهد شد."
#: frontend/src/pages/Programs/ProgramEnrollment.vue:145
msgid "Successfully enrolled in program"
@@ -6451,7 +6451,7 @@ msgstr ""
#: frontend/src/pages/QuizForm.vue:23
msgid "Test Quiz"
msgstr ""
msgstr "آزمون آزمایشی"
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseForm.vue:82
msgid "Test this Exercise"
@@ -6723,7 +6723,7 @@ msgstr "منطقه زمانی"
#: lms/lms/doctype/lms_course/lms_course.py:70
msgid "Timezone is required for paid certificates."
msgstr ""
msgstr "منطقه زمانی برای گواهینامه‌های پولی الزامی است."
#: lms/templates/emails/batch_confirmation.html:21
#: lms/templates/emails/batch_start_reminder.html:16
@@ -6840,7 +6840,7 @@ msgstr "توییتر"
#: frontend/src/components/Modals/EditProfile.vue:87
#: lms/fixtures/custom_field.json
msgid "Twitter ID"
msgstr ""
msgstr "شناسه توییتر"
#. Label of the type (Select) field in DocType 'Job Opportunity'
#. Label of a field in the job-opportunity Web Form
@@ -6976,7 +6976,7 @@ msgstr "در حال آپلود {0}%"
#: frontend/src/components/Settings/Coupons/CouponList.vue:182
#: lms/lms/doctype/lms_coupon/lms_coupon.json
msgid "Usage Limit"
msgstr ""
msgstr "محدودیت استفاده"
#: lms/lms/doctype/lms_coupon/lms_coupon.py:31
msgid "Usage limit cannot be negative"
@@ -6984,7 +6984,7 @@ msgstr "محدودیت استفاده نمی‌تواند منفی باشد"
#: frontend/src/components/Modals/EmailTemplateModal.vue:38
msgid "Use HTML"
msgstr ""
msgstr "استفاده از HTML"
#. Label of the user (Link) field in DocType 'LMS Job Application'
#. Label of the user (Link) field in DocType 'LMS Course Interest'
@@ -7366,7 +7366,7 @@ msgstr ""
#: frontend/src/pages/ProfileCertificates.vue:26
msgid "You have not received any certificates yet."
msgstr ""
msgstr "شما هنوز هیچ گواهی دریافت نکرده‌اید."
#: lms/lms/widgets/NoPreviewModal.html:12
msgid "You have opted to be notified for this course. You will receive an email when the course becomes available."
@@ -7483,7 +7483,7 @@ msgstr ""
#: frontend/src/components/Quiz.vue:258
msgid "Your submission has been successfully saved. The instructor will review and grade it shortly, and you'll be notified of your final result."
msgstr ""
msgstr "ارسال شما با موفقیت ذخیره شد. مدرس به زودی آن را بررسی و نمره‌دهی خواهد کرد و نتیجه نهایی به شما اطلاع داده خواهد شد."
#: frontend/src/pages/Lesson.vue:8
msgid "Zen Mode"
@@ -7662,15 +7662,15 @@ msgstr "دانش‌آموزان"
#: frontend/src/components/CommandPalette/CommandPalette.vue:59
msgid "to close"
msgstr ""
msgstr "برای بستن"
#: frontend/src/components/CommandPalette/CommandPalette.vue:45
msgid "to navigate"
msgstr ""
msgstr "برای پیمایش"
#: frontend/src/components/CommandPalette/CommandPalette.vue:53
msgid "to select"
msgstr ""
msgstr "برای انتخاب"
#: frontend/src/components/BatchFeedback.vue:12
msgid "to view your feedback."
@@ -7698,7 +7698,7 @@ msgstr ""
#: frontend/src/pages/Quizzes.vue:18
msgid "{0} Quizzes"
msgstr ""
msgstr "{0} آزمون"
#: lms/lms/api.py:722 lms/lms/api.py:730
msgid "{0} Settings not found"
+2 -2
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2025-12-19 16:05+0000\n"
"PO-Revision-Date: 2025-12-23 23:25\n"
"PO-Revision-Date: 2025-12-24 23:25\n"
"Last-Translator: jannat@frappe.io\n"
"Language-Team: Portuguese, Brazilian\n"
"MIME-Version: 1.0\n"
@@ -7750,7 +7750,7 @@ msgstr ""
#: lms/lms/api.py:771
msgid "{0} not found"
msgstr ""
msgstr "{0} não encontrado"
#. Count format of shortcut in the LMS Workspace
#: lms/lms/workspace/lms/lms.json
+2 -2
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2025-12-19 16:05+0000\n"
"PO-Revision-Date: 2025-12-23 23:25\n"
"PO-Revision-Date: 2025-12-24 23:25\n"
"Last-Translator: jannat@frappe.io\n"
"Language-Team: Russian\n"
"MIME-Version: 1.0\n"
@@ -7750,7 +7750,7 @@ msgstr "{0} упомянул вас в комментарии в {1}"
#: lms/lms/api.py:771
msgid "{0} not found"
msgstr "{0} не найдено"
msgstr "{0} не найден"
#. Count format of shortcut in the LMS Workspace
#: lms/lms/workspace/lms/lms.json
+16 -16
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2025-12-19 16:05+0000\n"
"PO-Revision-Date: 2025-12-23 23:25\n"
"PO-Revision-Date: 2025-12-25 23:32\n"
"Last-Translator: jannat@frappe.io\n"
"Language-Team: Serbian (Cyrillic)\n"
"MIME-Version: 1.0\n"
@@ -664,7 +664,7 @@ msgstr "Доступност је успешно ажурирана"
#: frontend/src/components/Modals/EvaluationModal.vue:26
msgid "Available Slots"
msgstr ""
msgstr "Доступни термини"
#: frontend/src/components/BatchFeedback.vue:43
msgid "Average Feedback Received"
@@ -2887,7 +2887,7 @@ msgstr "Преузми апликацију на свом иПхоне уређ
#: frontend/src/components/Modals/EditProfile.vue:84
msgid "GitHub ID"
msgstr ""
msgstr "GitHub ID"
#. Label of the github (Data) field in DocType 'User'
#: lms/fixtures/custom_field.json
@@ -3387,7 +3387,7 @@ msgstr "Приступ остварен у"
#: frontend/src/components/CommandPalette/CommandPalette.vue:132
#: frontend/src/components/CommandPalette/CommandPalette.vue:229
msgid "Jump to"
msgstr ""
msgstr "Иди на"
#: frontend/src/pages/Home/Streak.vue:18
msgid "Keep going,"
@@ -4503,7 +4503,7 @@ msgstr "Нема резултата"
#: frontend/src/components/Modals/EvaluationModal.vue:59
msgid "No slots available for the selected course."
msgstr ""
msgstr "Нема доступних термина за одабрану обуку."
#: frontend/src/components/Modals/VideoStatistics.vue:86
msgid "No statistics available for this video."
@@ -4665,7 +4665,7 @@ msgstr "Отвори "
#: frontend/src/components/UserAvatar.vue:11 frontend/src/pages/Profile.vue:61
#: lms/fixtures/custom_field.json
msgid "Open to Opportunities"
msgstr ""
msgstr "Отворен за прилике"
#. Label of the option (Data) field in DocType 'LMS Option'
#: frontend/src/components/Modals/Question.vue:70
@@ -5077,7 +5077,7 @@ msgstr "Молимо Вас да се пријавите да бисте се у
#: frontend/src/pages/Batch.vue:158
msgid "Please make sure to schedule your evaluation before this date."
msgstr ""
msgstr "Молимо Вас да закажете своје оцењивање пре овог датума."
#: lms/lms/notification/certificate_request_reminder/certificate_request_reminder.html:7
#: lms/templates/emails/certificate_request_notification.html:7
@@ -5114,7 +5114,7 @@ msgstr "Молимо Вас да изаберете квиз"
#: frontend/src/components/Modals/EvaluationModal.vue:109
msgid "Please select a slot for your evaluation."
msgstr ""
msgstr "Молимо Вас да одаберете термин за Ваше оцењивање."
#: frontend/src/components/Modals/LiveClassModal.vue:192
msgid "Please select a time."
@@ -5214,7 +5214,7 @@ msgstr "Пожељна локација"
#: frontend/src/pages/Search/Search.vue:41
msgid "Press enter to search"
msgstr ""
msgstr "Притисните ентер за претрагу"
#. Label of the prevent_skipping_videos (Check) field in DocType 'LMS Settings'
#: lms/lms/doctype/lms_settings/lms_settings.json
@@ -5832,7 +5832,7 @@ msgstr "Закажите оцењивање да бисте добили сер
#: frontend/src/components/Modals/EvaluationModal.vue:5
msgid "Schedule your evaluation"
msgstr ""
msgstr "Закажите своје оцењивање"
#. Name of a DocType
#: lms/lms/doctype/scheduled_flow/scheduled_flow.json
@@ -6048,7 +6048,7 @@ msgstr "Прикажи онлајн предавања"
#: frontend/src/components/Modals/EditProfile.vue:61
msgid "Show recruiters and others that you are open to work."
msgstr ""
msgstr "Покажите рекрутерима и другима да сте отворени за посао."
#. Label of the shuffle_questions (Check) field in DocType 'LMS Quiz'
#: frontend/src/pages/QuizForm.vue:105 lms/lms/doctype/lms_quiz/lms_quiz.json
@@ -6501,7 +6501,7 @@ msgstr "Особа која оцењује ову обуку није досту
#: frontend/src/pages/Batch.vue:151
msgid "The last day to schedule your evaluations is "
msgstr ""
msgstr "Последњи дан за заказивање Вашег оцењивања је "
#: lms/lms/utils.py:2083
msgid "The lesson does not exist."
@@ -6840,7 +6840,7 @@ msgstr "Twitter"
#: frontend/src/components/Modals/EditProfile.vue:87
#: lms/fixtures/custom_field.json
msgid "Twitter ID"
msgstr ""
msgstr "Twitter ID"
#. Label of the type (Select) field in DocType 'Job Opportunity'
#. Label of a field in the job-opportunity Web Form
@@ -7394,7 +7394,7 @@ msgstr "Морате бити модератор да бисте додељив
#: lms/lms/doctype/lms_course_review/lms_course_review.py:18
msgid "You must be enrolled in the course to submit a review"
msgstr ""
msgstr "Морате бити уписани на обуку да бисте поднели рецензију"
#: lms/lms/doctype/lms_enrollment/lms_enrollment.py:47
msgid "You need to complete the payment for this course before enrolling."
@@ -7620,11 +7620,11 @@ msgstr "онлајн предавања"
#: frontend/src/pages/Search/Search.vue:38
msgid "match"
msgstr ""
msgstr "резултат"
#: frontend/src/pages/Search/Search.vue:38
msgid "matches"
msgstr ""
msgstr "резултати"
#: frontend/src/pages/Programs/Programs.vue:42
#: frontend/src/pages/Programs/StudentPrograms.vue:36
+16 -16
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2025-12-19 16:05+0000\n"
"PO-Revision-Date: 2025-12-23 23:25\n"
"PO-Revision-Date: 2025-12-25 23:32\n"
"Last-Translator: jannat@frappe.io\n"
"Language-Team: Serbian (Latin)\n"
"MIME-Version: 1.0\n"
@@ -664,7 +664,7 @@ msgstr "Dostupnost je uspešno ažurirana"
#: frontend/src/components/Modals/EvaluationModal.vue:26
msgid "Available Slots"
msgstr ""
msgstr "Dostupni termini"
#: frontend/src/components/BatchFeedback.vue:43
msgid "Average Feedback Received"
@@ -2887,7 +2887,7 @@ msgstr "Preuzmi aplikaciju na svom iPhone uređaju za lakši pristup i bolje kor
#: frontend/src/components/Modals/EditProfile.vue:84
msgid "GitHub ID"
msgstr ""
msgstr "GitHub ID"
#. Label of the github (Data) field in DocType 'User'
#: lms/fixtures/custom_field.json
@@ -3387,7 +3387,7 @@ msgstr "Pristup ostvaren u"
#: frontend/src/components/CommandPalette/CommandPalette.vue:132
#: frontend/src/components/CommandPalette/CommandPalette.vue:229
msgid "Jump to"
msgstr ""
msgstr "Idi na"
#: frontend/src/pages/Home/Streak.vue:18
msgid "Keep going,"
@@ -4503,7 +4503,7 @@ msgstr "Nema rezultata"
#: frontend/src/components/Modals/EvaluationModal.vue:59
msgid "No slots available for the selected course."
msgstr ""
msgstr "Nema dostupnih termina za odabranu obuku."
#: frontend/src/components/Modals/VideoStatistics.vue:86
msgid "No statistics available for this video."
@@ -4665,7 +4665,7 @@ msgstr "Otvori "
#: frontend/src/components/UserAvatar.vue:11 frontend/src/pages/Profile.vue:61
#: lms/fixtures/custom_field.json
msgid "Open to Opportunities"
msgstr ""
msgstr "Otvoren za prilike"
#. Label of the option (Data) field in DocType 'LMS Option'
#: frontend/src/components/Modals/Question.vue:70
@@ -5077,7 +5077,7 @@ msgstr "Molimo Vas da se prijavite da biste se upisali u program."
#: frontend/src/pages/Batch.vue:158
msgid "Please make sure to schedule your evaluation before this date."
msgstr ""
msgstr "Molimo Vas da zakažete svoje ocenjivanje pre ovog datuma."
#: lms/lms/notification/certificate_request_reminder/certificate_request_reminder.html:7
#: lms/templates/emails/certificate_request_notification.html:7
@@ -5114,7 +5114,7 @@ msgstr "Molimo Vas da izaberete kviz"
#: frontend/src/components/Modals/EvaluationModal.vue:109
msgid "Please select a slot for your evaluation."
msgstr ""
msgstr "Molimo Vas da odaberete termin za Vaše ocenjivanje."
#: frontend/src/components/Modals/LiveClassModal.vue:192
msgid "Please select a time."
@@ -5214,7 +5214,7 @@ msgstr "Poželjna lokacija"
#: frontend/src/pages/Search/Search.vue:41
msgid "Press enter to search"
msgstr ""
msgstr "Pritisnite enter za pretragu"
#. Label of the prevent_skipping_videos (Check) field in DocType 'LMS Settings'
#: lms/lms/doctype/lms_settings/lms_settings.json
@@ -5832,7 +5832,7 @@ msgstr "Zakažite ocenjivanje da biste dobili sertifikat."
#: frontend/src/components/Modals/EvaluationModal.vue:5
msgid "Schedule your evaluation"
msgstr ""
msgstr "Zakažite svoje ocenjivanje"
#. Name of a DocType
#: lms/lms/doctype/scheduled_flow/scheduled_flow.json
@@ -6048,7 +6048,7 @@ msgstr "Prikaži onlajn predavanja"
#: frontend/src/components/Modals/EditProfile.vue:61
msgid "Show recruiters and others that you are open to work."
msgstr ""
msgstr "Pokažite rekruterima i drugima da ste otvoreni za posao."
#. Label of the shuffle_questions (Check) field in DocType 'LMS Quiz'
#: frontend/src/pages/QuizForm.vue:105 lms/lms/doctype/lms_quiz/lms_quiz.json
@@ -6501,7 +6501,7 @@ msgstr "Osoba koja ocenjuje ovu obuku nije dostupna u periodu od {0} do {1}. Mol
#: frontend/src/pages/Batch.vue:151
msgid "The last day to schedule your evaluations is "
msgstr ""
msgstr "Poslednji dan za zakazivanje Vašeg ocenjivanja je "
#: lms/lms/utils.py:2083
msgid "The lesson does not exist."
@@ -6840,7 +6840,7 @@ msgstr "Twitter"
#: frontend/src/components/Modals/EditProfile.vue:87
#: lms/fixtures/custom_field.json
msgid "Twitter ID"
msgstr ""
msgstr "Twitter ID"
#. Label of the type (Select) field in DocType 'Job Opportunity'
#. Label of a field in the job-opportunity Web Form
@@ -7394,7 +7394,7 @@ msgstr "Morate biti moderator da biste dodeljivali bedževe korisnicima."
#: lms/lms/doctype/lms_course_review/lms_course_review.py:18
msgid "You must be enrolled in the course to submit a review"
msgstr ""
msgstr "Morate biti upisani na obuku da biste podneli recenziju"
#: lms/lms/doctype/lms_enrollment/lms_enrollment.py:47
msgid "You need to complete the payment for this course before enrolling."
@@ -7620,11 +7620,11 @@ msgstr "onlajn predavanja"
#: frontend/src/pages/Search/Search.vue:38
msgid "match"
msgstr ""
msgstr "rezultat"
#: frontend/src/pages/Search/Search.vue:38
msgid "matches"
msgstr ""
msgstr "rezultati"
#: frontend/src/pages/Programs/Programs.vue:42
#: frontend/src/pages/Programs/StudentPrograms.vue:36
+2 -2
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2025-12-19 16:05+0000\n"
"PO-Revision-Date: 2025-12-23 23:25\n"
"PO-Revision-Date: 2025-12-24 23:25\n"
"Last-Translator: jannat@frappe.io\n"
"Language-Team: Turkish\n"
"MIME-Version: 1.0\n"
@@ -7750,7 +7750,7 @@ msgstr ""
#: lms/lms/api.py:771
msgid "{0} not found"
msgstr "{0} bulunamadı"
msgstr ""
#. Count format of shortcut in the LMS Workspace
#: lms/lms/workspace/lms/lms.json
+2 -2
View File
@@ -3,7 +3,7 @@ msgstr ""
"Project-Id-Version: frappe\n"
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2025-12-19 16:05+0000\n"
"PO-Revision-Date: 2025-12-23 23:25\n"
"PO-Revision-Date: 2025-12-24 23:25\n"
"Last-Translator: jannat@frappe.io\n"
"Language-Team: Chinese Simplified\n"
"MIME-Version: 1.0\n"
@@ -7750,7 +7750,7 @@ msgstr "{0}在{1}的评论中提及您"
#: lms/lms/api.py:771
msgid "{0} not found"
msgstr "{0}未找到"
msgstr "未找到{0}"
#. Count format of shortcut in the LMS Workspace
#: lms/lms/workspace/lms/lms.json
+2 -1
View File
@@ -113,4 +113,5 @@ lms.patches.v2_0.enable_programming_exercises_in_sidebar
lms.patches.v2_0.count_in_program
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.fix_job_application_resume_urls
lms.patches.v2_0.fix_job_application_resume_urls
lms.patches.v2_0.open_to_opportunities
+10
View File
@@ -0,0 +1,10 @@
import frappe
def execute():
looking_for_job = frappe.get_all("User", {"looking_for_job": 1}, ["name"])
for user in looking_for_job:
frappe.db.set_value("User", user.name, "open_to", "Opportunities")
frappe.db.delete("Custom Field", {"dt": "User", "fieldname": "looking_for_job"})
+1 -1
View File
@@ -12,7 +12,7 @@ dependencies = [
"websocket_client~=1.6.4",
"markdown~=3.5.1",
"beautifulsoup4>=4.12,<4.14",
"lxml~=4.9.3",
"lxml~=6.0.2",
"cairocffi==1.5.1",
"razorpay~=1.4.1",
"fuzzywuzzy~=0.18.0",