mirror of
https://github.com/frappe/lms.git
synced 2026-04-19 22:52:29 +03:00
Merge pull request #2303 from frappe/develop
chore: merge `develop` into `main-hotfix`
This commit is contained in:
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -38,9 +38,9 @@ jobs:
|
||||
|
||||
- name: Set Branch
|
||||
run: |
|
||||
export APPS_JSON='[{"url": "https://github.com/frappe/lms","branch": "main"}]'
|
||||
export APPS_JSON='[{"url": "https://github.com/frappe/payments","branch": "version-15"},{"url": "https://github.com/frappe/lms","branch": "main"}]'
|
||||
echo "APPS_JSON_BASE64=$(echo $APPS_JSON | base64 -w 0)" >> $GITHUB_ENV
|
||||
echo "FRAPPE_BRANCH=version-15" >> $GITHUB_ENV
|
||||
echo "FRAPPE_BRANCH=version-16" >> $GITHUB_ENV
|
||||
|
||||
- name: Set Image Tag
|
||||
run: |
|
||||
@@ -61,4 +61,4 @@ jobs:
|
||||
ghcr.io/${{ github.repository }}:${{ env.IMAGE_TAG }}
|
||||
build-args: |
|
||||
"FRAPPE_BRANCH=${{ env.FRAPPE_BRANCH }}"
|
||||
"APPS_JSON_BASE64=${{ env.APPS_JSON_BASE64 }}"
|
||||
"APPS_JSON_BASE64=${{ env.APPS_JSON_BASE64 }}"
|
||||
|
||||
@@ -24,6 +24,7 @@ bench set-redis-socketio-host redis://redis:6379
|
||||
sed -i '/redis/d' ./Procfile
|
||||
sed -i '/watch/d' ./Procfile
|
||||
|
||||
bench get-app payments
|
||||
bench get-app lms
|
||||
|
||||
bench new-site lms.localhost \
|
||||
@@ -32,6 +33,7 @@ bench new-site lms.localhost \
|
||||
--admin-password admin \
|
||||
--no-mariadb-socket
|
||||
|
||||
bench --site lms.localhost install-app payments
|
||||
bench --site lms.localhost install-app lms
|
||||
bench --site lms.localhost set-config developer_mode 1
|
||||
bench --site lms.localhost clear-cache
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-ink-gray-9 font-semibold mb-5">
|
||||
{{ __('Assignment Question') }}
|
||||
{{ __('Assignment') }}: {{ assignment.data.title }}
|
||||
</div>
|
||||
<div
|
||||
v-html="assignment.data.question"
|
||||
@@ -300,7 +300,7 @@ const submitAssignment = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const addNewSubmission = () => {
|
||||
const prepareSubmissionDoc = () => {
|
||||
let doc = {
|
||||
doctype: 'LMS Assignment Submission',
|
||||
assignment: props.assignmentID,
|
||||
@@ -311,24 +311,31 @@ const addNewSubmission = () => {
|
||||
} else {
|
||||
doc.assignment_attachment = attachment.value
|
||||
}
|
||||
return doc
|
||||
}
|
||||
|
||||
const addNewSubmission = () => {
|
||||
let doc = prepareSubmissionDoc()
|
||||
if (!doc.assignment_attachment && !doc.answer) {
|
||||
toast.error(
|
||||
__('Please provide an answer or upload a file before submitting.')
|
||||
)
|
||||
return
|
||||
}
|
||||
call('frappe.client.insert', {
|
||||
doc: doc,
|
||||
})
|
||||
.then((data) => {
|
||||
toast.success(__('Assignment submitted successfully'))
|
||||
if (router.currentRoute.value.name == 'AssignmentSubmission') {
|
||||
router.push({
|
||||
name: 'AssignmentSubmission',
|
||||
params: {
|
||||
assignmentID: props.assignmentID,
|
||||
submissionName: data.name,
|
||||
},
|
||||
query: { fromLesson: router.currentRoute.value.query.fromLesson },
|
||||
})
|
||||
} else {
|
||||
markLessonProgress()
|
||||
router.go()
|
||||
}
|
||||
router.push({
|
||||
name: 'AssignmentSubmission',
|
||||
params: {
|
||||
assignmentID: props.assignmentID,
|
||||
submissionName: data.name,
|
||||
},
|
||||
query: { fromLesson: router.currentRoute.value.query.fromLesson },
|
||||
})
|
||||
markLessonProgress()
|
||||
isDirty.value = false
|
||||
submissionResource.name = data.name
|
||||
submissionResource.reload()
|
||||
@@ -372,15 +379,17 @@ const saveSubmission = (file) => {
|
||||
}
|
||||
|
||||
const markLessonProgress = () => {
|
||||
if (router.currentRoute.value.name == 'Lesson') {
|
||||
let courseName = router.currentRoute.value.params.courseName
|
||||
let chapterNumber = router.currentRoute.value.params.chapterNumber
|
||||
let lessonNumber = router.currentRoute.value.params.lessonNumber
|
||||
let pathname = window.location.pathname.split('/')
|
||||
if (!pathname.includes('courses'))
|
||||
pathname = window.parent.location.pathname.split('/')
|
||||
if (pathname[2] != 'courses') return
|
||||
let lessonIndex = pathname.pop().split('-')
|
||||
|
||||
if (lessonIndex.length == 2) {
|
||||
call('lms.lms.api.mark_lesson_progress', {
|
||||
course: courseName,
|
||||
chapter_number: chapterNumber,
|
||||
lesson_number: lessonNumber,
|
||||
course: pathname[3],
|
||||
chapter_number: lessonIndex[0],
|
||||
lesson_number: lessonIndex[1],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="course.title"
|
||||
class="flex flex-col h-full rounded-md overflow-auto text-ink-gray-9"
|
||||
class="flex flex-col h-full rounded-md overflow-auto text-ink-gray-9 bg-surface-cards"
|
||||
style="min-height: 350px"
|
||||
>
|
||||
<div
|
||||
@@ -10,7 +10,7 @@
|
||||
course.image
|
||||
? { backgroundImage: `url('${encodeURI(course.image)}')` }
|
||||
: {
|
||||
backgroundImage: getGradientColor(),
|
||||
backgroundImage: gradientColor,
|
||||
backgroundBlendMode: 'screen',
|
||||
}
|
||||
"
|
||||
@@ -137,6 +137,8 @@ import { Award, BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { Tooltip } from 'frappe-ui'
|
||||
import { formatAmount } from '@/utils'
|
||||
import { theme } from '@/utils/theme'
|
||||
import { computed, watch } from 'vue'
|
||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import ProgressBar from '@/components/ProgressBar.vue'
|
||||
@@ -151,12 +153,12 @@ const props = defineProps({
|
||||
},
|
||||
})
|
||||
|
||||
const getGradientColor = () => {
|
||||
let theme = localStorage.getItem('theme') == 'dark' ? 'darkMode' : 'lightMode'
|
||||
const gradientColor = computed(() => {
|
||||
let themeMode = theme.value === 'dark' ? 'darkMode' : 'lightMode'
|
||||
let color = props.course.card_gradient?.toLowerCase() || 'blue'
|
||||
let colorMap = colors[theme][color]
|
||||
let colorMap = colors[themeMode][color]
|
||||
return `linear-gradient(to top right, black, ${colorMap[400]})`
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style>
|
||||
.course-card-pills {
|
||||
|
||||
@@ -57,7 +57,7 @@
|
||||
import { getSidebarLinks } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { call } from 'frappe-ui'
|
||||
import { watch, ref, onMounted } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import { usersStore } from '@/stores/user'
|
||||
@@ -68,26 +68,13 @@ let { isLoggedIn } = sessionStore()
|
||||
const { sidebarSettings } = useSettings()
|
||||
const router = useRouter()
|
||||
let { userResource } = usersStore()
|
||||
const sidebarLinks = ref(getSidebarLinks())
|
||||
const sidebarLinks = ref([])
|
||||
const otherLinks = ref([])
|
||||
const showMenu = ref(false)
|
||||
const menu = ref(null)
|
||||
const isModerator = ref(false)
|
||||
const isInstructor = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
sidebarSettings.reload(
|
||||
{},
|
||||
{
|
||||
onSuccess(data) {
|
||||
destructureSidebarLinks()
|
||||
filterLinksToShow(data)
|
||||
addOtherLinks()
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
const handleOutsideClick = (e) => {
|
||||
if (menu.value && !menu.value.contains(e.target)) {
|
||||
showMenu.value = false
|
||||
@@ -126,65 +113,57 @@ const filterLinksToShow = (data) => {
|
||||
|
||||
const addOtherLinks = () => {
|
||||
if (user) {
|
||||
otherLinks.value.push({
|
||||
label: 'Notifications',
|
||||
icon: 'Bell',
|
||||
to: 'Notifications',
|
||||
})
|
||||
otherLinks.value.push({
|
||||
label: 'Profile',
|
||||
icon: 'UserRound',
|
||||
})
|
||||
otherLinks.value.push({
|
||||
label: 'Log out',
|
||||
icon: 'LogOut',
|
||||
})
|
||||
addLink('Notifications', 'Bell', 'Notifications')
|
||||
addLink('Profile', 'UserRound')
|
||||
addLink('Log out', 'LogOut')
|
||||
} else {
|
||||
otherLinks.value.push({
|
||||
label: 'Log in',
|
||||
icon: 'LogIn',
|
||||
})
|
||||
addLink('Log in', 'LogIn')
|
||||
}
|
||||
}
|
||||
|
||||
watch(userResource, () => {
|
||||
if (userResource.data) {
|
||||
isModerator.value = userResource.data.is_moderator
|
||||
isInstructor.value = userResource.data.is_instructor
|
||||
addPrograms()
|
||||
if (isModerator.value || isInstructor.value) {
|
||||
addProgrammingExercises()
|
||||
addQuizzes()
|
||||
addAssignments()
|
||||
const addLink = (label, icon, to = '') => {
|
||||
if (otherLinks.value.some((link) => link.label === label)) return
|
||||
otherLinks.value.push({
|
||||
label: label,
|
||||
icon: icon,
|
||||
to: to,
|
||||
})
|
||||
}
|
||||
|
||||
const updateSidebarLinks = () => {
|
||||
sidebarLinks.value = getSidebarLinks(true)
|
||||
destructureSidebarLinks()
|
||||
sidebarSettings.reload(
|
||||
{},
|
||||
{
|
||||
onSuccess: async (data) => {
|
||||
filterLinksToShow(data)
|
||||
await addPrograms()
|
||||
if (isModerator.value || isInstructor.value) {
|
||||
addQuizzes()
|
||||
addAssignments()
|
||||
addProgrammingExercises()
|
||||
}
|
||||
addOtherLinks()
|
||||
},
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const addQuizzes = () => {
|
||||
otherLinks.value.push({
|
||||
label: 'Quizzes',
|
||||
icon: 'CircleHelp',
|
||||
to: 'Quizzes',
|
||||
})
|
||||
addLink('Quizzes', 'CircleHelp', 'Quizzes')
|
||||
}
|
||||
|
||||
const addAssignments = () => {
|
||||
otherLinks.value.push({
|
||||
label: 'Assignments',
|
||||
icon: 'Pencil',
|
||||
to: 'Assignments',
|
||||
})
|
||||
addLink('Assignments', 'Pencil', 'Assignments')
|
||||
}
|
||||
|
||||
const addProgrammingExercises = () => {
|
||||
otherLinks.value.push({
|
||||
label: 'Programming Exercises',
|
||||
icon: 'Code',
|
||||
to: 'ProgrammingExercises',
|
||||
})
|
||||
addLink('Programming Exercises', 'Code', 'ProgrammingExercises')
|
||||
}
|
||||
|
||||
const addPrograms = async () => {
|
||||
if (sidebarLinks.value.some((link) => link.label === 'Programs')) return
|
||||
let canAddProgram = await checkIfCanAddProgram()
|
||||
if (!canAddProgram) return
|
||||
let activeFor = ['Programs', 'ProgramDetail']
|
||||
@@ -198,7 +177,21 @@ const addPrograms = async () => {
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
userResource,
|
||||
async () => {
|
||||
await userResource.promise
|
||||
if (userResource.data) {
|
||||
isModerator.value = userResource.data.is_moderator
|
||||
isInstructor.value = userResource.data.is_instructor
|
||||
}
|
||||
updateSidebarLinks()
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const checkIfCanAddProgram = async () => {
|
||||
if (!userResource.data) return false
|
||||
if (isModerator.value || isInstructor.value) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -224,6 +224,7 @@
|
||||
</div>
|
||||
<div class="flex items-center justify-between mt-8">
|
||||
<Checkbox
|
||||
v-if="!quiz.data.show_answers"
|
||||
:label="__('Mark for review')"
|
||||
:model-value="reviewQuestions.includes(activeQuestion) ? 1 : 0"
|
||||
@change="markForReview($event, activeQuestion)"
|
||||
@@ -278,6 +279,7 @@
|
||||
!showAnswers.length &&
|
||||
questionDetails.data.type != 'Open Ended'
|
||||
"
|
||||
class="ml-auto"
|
||||
@click="checkAnswer()"
|
||||
>
|
||||
<span>
|
||||
@@ -289,12 +291,18 @@
|
||||
activeQuestion != questions.length && quiz.data.show_answers
|
||||
"
|
||||
@click="nextQuestion()"
|
||||
class="ml-auto"
|
||||
>
|
||||
<span>
|
||||
{{ __('Next') }}
|
||||
</span>
|
||||
</Button>
|
||||
<Button variant="solid" v-else @click="handleSubmitClick()">
|
||||
<Button
|
||||
variant="solid"
|
||||
v-else
|
||||
@click="handleSubmitClick()"
|
||||
class="ml-auto"
|
||||
>
|
||||
<span>
|
||||
{{ __('Submit') }}
|
||||
</span>
|
||||
@@ -891,10 +899,14 @@ const markLessonProgress = () => {
|
||||
}
|
||||
|
||||
const handleSubmitClick = () => {
|
||||
if (attemptedQuestions.value.length) {
|
||||
switchQuestion(activeQuestion.value)
|
||||
if (!quiz.data.show_answers) {
|
||||
if (attemptedQuestions.value.length) {
|
||||
switchQuestion(activeQuestion.value)
|
||||
}
|
||||
showSubmissionConfirmation.value = true
|
||||
} else {
|
||||
submitQuiz()
|
||||
}
|
||||
showSubmissionConfirmation.value = true
|
||||
}
|
||||
|
||||
const paginationWindow = computed(() => {
|
||||
|
||||
@@ -32,16 +32,14 @@
|
||||
</div>
|
||||
<div v-if="transactionData" class="overflow-y-auto">
|
||||
<div class="grid grid-cols-3 gap-5">
|
||||
<Switch
|
||||
size="sm"
|
||||
<FormControl
|
||||
:label="__('Payment Received')"
|
||||
:description="__('Mark the payment as received.')"
|
||||
type="checkbox"
|
||||
v-model="transactionData.payment_received"
|
||||
/>
|
||||
<Switch
|
||||
size="sm"
|
||||
<FormControl
|
||||
:label="__('Payment For Certificate')"
|
||||
:description="__('This payment is for a certificate.')"
|
||||
type="checkbox"
|
||||
v-model="transactionData.payment_for_certificate"
|
||||
/>
|
||||
<FormControl
|
||||
|
||||
@@ -27,17 +27,15 @@
|
||||
doctype="User"
|
||||
:placeholder="__('Filter by Member')"
|
||||
/>
|
||||
<Switch
|
||||
size="sm"
|
||||
:label="__('Payment Received')"
|
||||
:description="__('Mark the payment as received.')"
|
||||
<FormControl
|
||||
v-model="paymentReceived"
|
||||
type="checkbox"
|
||||
:label="__('Payment Received')"
|
||||
/>
|
||||
<Switch
|
||||
size="sm"
|
||||
:label="__('Payment For Certificate')"
|
||||
:description="__('This payment is for a certificate.')"
|
||||
<FormControl
|
||||
v-model="paymentForCertificate"
|
||||
type="checkbox"
|
||||
:label="__('Payment for Certificate')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -47,12 +45,12 @@
|
||||
:rows="transactions.data"
|
||||
row-key="name"
|
||||
:options="{
|
||||
showTooltip: false,
|
||||
selectable: false,
|
||||
onRowClick: (row: { [key: string]: any }) => {
|
||||
openForm(row)
|
||||
},
|
||||
}"
|
||||
showTooltip: false,
|
||||
selectable: false,
|
||||
onRowClick: (row: { [key: string]: any }) => {
|
||||
openForm(row)
|
||||
},
|
||||
}"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||
|
||||
@@ -68,6 +68,7 @@ import { sessionStore } from '@/stores/session'
|
||||
import { call, Dropdown, toast } from 'frappe-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { convertToTitleCase } from '@/utils'
|
||||
import { applyTheme, toggleTheme, theme } from '@/utils/theme'
|
||||
import { usersStore } from '@/stores/user'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import { markRaw, watch, ref, onMounted, computed } from 'vue'
|
||||
@@ -94,7 +95,6 @@ let { userResource } = usersStore()
|
||||
const settingsStore = useSettings()
|
||||
let { isLoggedIn } = sessionStore()
|
||||
const showSettingsModal = ref(false)
|
||||
const theme = ref('light')
|
||||
const frappeCloudBaseEndpoint = 'https://frappecloud.com'
|
||||
const $dialog = createDialog
|
||||
|
||||
@@ -106,9 +106,8 @@ const props = defineProps({
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
theme.value = localStorage.getItem('theme') || 'light'
|
||||
if (['light', 'dark'].includes(theme.value)) {
|
||||
document.documentElement.setAttribute('data-theme', theme.value)
|
||||
applyTheme(theme.value)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -119,13 +118,6 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
const toggleTheme = () => {
|
||||
const currentTheme = document.documentElement.getAttribute('data-theme')
|
||||
theme.value = currentTheme === 'dark' ? 'light' : 'dark'
|
||||
document.documentElement.setAttribute('data-theme', theme.value)
|
||||
localStorage.setItem('theme', theme.value)
|
||||
}
|
||||
|
||||
const userDropdownOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -20,18 +20,15 @@
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div class="md:w-3/4 md:mx-auto py-5 mx-5">
|
||||
<div class="py-5 mx-5">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div v-if="assignmentCount" class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ __('{0} Assignments').format(assignmentCount) }}
|
||||
<div class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ __('{0} Assignments').format(assignments.data?.length) }}
|
||||
</div>
|
||||
<div
|
||||
v-if="assignments.data?.length || assignmentCount > 0"
|
||||
class="grid grid-cols-2 gap-5"
|
||||
>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
v-model="titleFilter"
|
||||
:placeholder="__('Search by title')"
|
||||
:placeholder="__('Search by Title')"
|
||||
/>
|
||||
<FormControl
|
||||
v-model="typeFilter"
|
||||
@@ -48,23 +45,75 @@
|
||||
row-key="name"
|
||||
:options="{
|
||||
showTooltip: false,
|
||||
selectable: false,
|
||||
selectable: true,
|
||||
onRowClick: (row) => {
|
||||
if (readOnlyMode) return
|
||||
assignmentID = row.name
|
||||
showAssignmentForm = true
|
||||
},
|
||||
}"
|
||||
class="h-[79vh] border-b"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none p-2"
|
||||
>
|
||||
<ListHeaderItem :item="item" v-for="item in assignmentColumns">
|
||||
<template #prefix="{ item }">
|
||||
<FeatherIcon :name="item.icon?.toString()" class="h-4 w-4" />
|
||||
</template>
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<ListRow
|
||||
v-for="row in assignments.data"
|
||||
:row="row"
|
||||
class="hover:bg-surface-gray-2"
|
||||
>
|
||||
<template #default="{ column, item }">
|
||||
<ListRowItem :item="row[column.key]" :align="column.align">
|
||||
<div v-if="column.key == 'show_answers'">
|
||||
<FormControl
|
||||
type="checkbox"
|
||||
v-model="row[column.key]"
|
||||
:disabled="true"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="column.key == 'modified'"
|
||||
class="text-sm text-ink-gray-5"
|
||||
>
|
||||
{{ row[column.key] }}
|
||||
</div>
|
||||
<div v-else>
|
||||
{{ row[column.key] }}
|
||||
</div>
|
||||
</ListRowItem>
|
||||
</template>
|
||||
</ListRow>
|
||||
</ListRows>
|
||||
<ListSelectBanner class="bottom-50">
|
||||
<template #actions="{ unselectAll, selections }">
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
@click="deleteAssignment(selections, unselectAll)"
|
||||
>
|
||||
<FeatherIcon name="trash-2" class="h-4 w-4 stroke-1.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</ListSelectBanner>
|
||||
</ListView>
|
||||
<EmptyState v-else type="Assignments" />
|
||||
<div
|
||||
v-if="assignments.data && assignments.hasNextPage"
|
||||
class="flex justify-center my-5"
|
||||
>
|
||||
<Button @click="assignments.next()">
|
||||
<div class="flex items-center justify-end space-x-3 mt-3">
|
||||
<Button v-if="assignments.hasNextPage" @click="assignments.next()">
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
<div v-if="assignments.hasNextPage" class="h-8 border-l"></div>
|
||||
<div class="text-ink-gray-5">
|
||||
{{ assignments.data?.length }} {{ __('of') }}
|
||||
{{ totalAssignments.data }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AssignmentForm
|
||||
@@ -79,8 +128,17 @@ import {
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
createResource,
|
||||
FormControl,
|
||||
ListView,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListRowItem,
|
||||
ListSelectBanner,
|
||||
FeatherIcon,
|
||||
toast,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
@@ -96,7 +154,6 @@ const titleFilter = ref('')
|
||||
const typeFilter = ref('')
|
||||
const showAssignmentForm = ref(false)
|
||||
const assignmentID = ref('new')
|
||||
const assignmentCount = ref(0)
|
||||
const { brand } = sessionStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
@@ -110,7 +167,6 @@ onMounted(() => {
|
||||
assignmentID.value = 'new'
|
||||
showAssignmentForm.value = true
|
||||
}
|
||||
getAssignmentCount()
|
||||
titleFilter.value = router.currentRoute.value.query.title
|
||||
typeFilter.value = router.currentRoute.value.query.type
|
||||
})
|
||||
@@ -123,6 +179,9 @@ watch([titleFilter, typeFilter], () => {
|
||||
},
|
||||
})
|
||||
reloadAssignments()
|
||||
totalAssignments.update({
|
||||
filters: assignmentFilter.value,
|
||||
})
|
||||
})
|
||||
|
||||
const reloadAssignments = () => {
|
||||
@@ -137,7 +196,7 @@ const assignmentFilter = computed(() => {
|
||||
if (titleFilter.value) {
|
||||
filters.title = ['like', `%${titleFilter.value}%`]
|
||||
}
|
||||
if (typeFilter.value) {
|
||||
if (typeFilter.value && typeFilter.value.trim() !== '') {
|
||||
filters.type = typeFilter.value
|
||||
}
|
||||
return filters
|
||||
@@ -145,51 +204,60 @@ const assignmentFilter = computed(() => {
|
||||
|
||||
const assignments = createListResource({
|
||||
doctype: 'LMS Assignment',
|
||||
fields: ['name', 'title', 'type', 'creation', 'question', 'course'],
|
||||
fields: ['name', 'title', 'type', 'modified', 'question', 'course'],
|
||||
orderBy: 'modified desc',
|
||||
cache: ['assignments'],
|
||||
transform(data) {
|
||||
return data.map((row) => {
|
||||
return {
|
||||
...row,
|
||||
creation: dayjs(row.creation).fromNow(),
|
||||
modified: dayjs(row.modified).format('DD MMM YYYY'),
|
||||
}
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const totalAssignments = createResource({
|
||||
url: 'frappe.client.get_count',
|
||||
params: {
|
||||
doctype: 'LMS Assignment',
|
||||
filters: assignmentFilter.value,
|
||||
},
|
||||
auto: true,
|
||||
cache: ['assignments_count', user.data?.name],
|
||||
onError(err) {
|
||||
toast.error(err.messages?.[0] || err)
|
||||
console.error(err)
|
||||
},
|
||||
})
|
||||
|
||||
const assignmentColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: __('Title'),
|
||||
key: 'title',
|
||||
width: 2,
|
||||
width: 1,
|
||||
icon: 'file-text',
|
||||
},
|
||||
{
|
||||
label: __('Type'),
|
||||
key: 'type',
|
||||
width: 1,
|
||||
align: 'left',
|
||||
icon: 'tag',
|
||||
},
|
||||
{
|
||||
label: __('Created'),
|
||||
key: 'creation',
|
||||
label: __('Modified'),
|
||||
key: 'modified',
|
||||
width: 1,
|
||||
align: 'right',
|
||||
icon: 'clock',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
const getAssignmentCount = () => {
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Assignment',
|
||||
}).then((data) => {
|
||||
assignmentCount.value = data
|
||||
})
|
||||
}
|
||||
|
||||
const assignmentTypes = computed(() => {
|
||||
let types = ['', 'Document', 'Image', 'PDF', 'URL', 'Text']
|
||||
let types = [' ', 'Document', 'Image', 'PDF', 'URL', 'Text']
|
||||
return types.map((type) => {
|
||||
return {
|
||||
label: __(type),
|
||||
@@ -198,6 +266,14 @@ const assignmentTypes = computed(() => {
|
||||
})
|
||||
})
|
||||
|
||||
const deleteAssignment = (selections, unselectAll) => {
|
||||
Array.from(selections).forEach(async (assignmentName) => {
|
||||
await assignments.delete.submit(assignmentName)
|
||||
})
|
||||
unselectAll()
|
||||
toast.success(__('Assignments deleted successfully'))
|
||||
}
|
||||
|
||||
const breadcrumbs = computed(() => [
|
||||
{
|
||||
label: __('Assignments'),
|
||||
|
||||
@@ -199,8 +199,15 @@
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
<Button variant="solid" size="md" @click="generatePaymentLink()">
|
||||
{{ __('Proceed to Payment') }}
|
||||
<Button
|
||||
variant="solid"
|
||||
size="md"
|
||||
class="ms-auto"
|
||||
@click="generatePaymentLink()"
|
||||
>
|
||||
{{
|
||||
isZeroAmount ? __('Enroll for Free') : __('Proceed to Payment')
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -326,16 +333,10 @@ const paymentLink = createResource({
|
||||
let data = {
|
||||
doctype: props.type == 'batch' ? 'LMS Batch' : 'LMS Course',
|
||||
docname: props.name,
|
||||
title: orderSummary.data.title,
|
||||
amount: orderSummary.data.original_amount,
|
||||
discount_amount: orderSummary.data.discount_amount || 0,
|
||||
gst_amount: orderSummary.data.gst_applied || 0,
|
||||
currency: orderSummary.data.currency,
|
||||
address: billingDetails,
|
||||
redirect_to: redirectTo.value,
|
||||
payment_for_certificate: props.type == 'certificate',
|
||||
coupon_code: appliedCoupon.value,
|
||||
coupon: orderSummary.data.coupon,
|
||||
country: billingDetails.country,
|
||||
}
|
||||
return data
|
||||
},
|
||||
@@ -458,14 +459,8 @@ const changeCurrency = (country) => {
|
||||
orderSummary.reload()
|
||||
}
|
||||
|
||||
const redirectTo = computed(() => {
|
||||
if (props.type == 'course') {
|
||||
return getLmsRoute(`courses/${props.name}`)
|
||||
} else if (props.type == 'batch') {
|
||||
return getLmsRoute(`batches/${props.name}`)
|
||||
} else if (props.type == 'certificate') {
|
||||
return getLmsRoute(`courses/${props.name}/certification`)
|
||||
}
|
||||
const isZeroAmount = computed(() => {
|
||||
return orderSummary.data && parseFloat(orderSummary.data.total_amount) <= 0
|
||||
})
|
||||
|
||||
watch(billingDetails, () => {
|
||||
|
||||
@@ -41,16 +41,16 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<Switch
|
||||
size="sm"
|
||||
<FormControl
|
||||
v-model="openToWork"
|
||||
:label="__('Open to Work')"
|
||||
type="checkbox"
|
||||
@change="updateParticipants()"
|
||||
/>
|
||||
<Switch
|
||||
size="sm"
|
||||
<FormControl
|
||||
v-model="hiring"
|
||||
:label="__('Hiring')"
|
||||
type="checkbox"
|
||||
@change="updateParticipants()"
|
||||
/>
|
||||
</div>
|
||||
@@ -129,7 +129,6 @@ import {
|
||||
createListResource,
|
||||
FormControl,
|
||||
Select,
|
||||
Switch,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
|
||||
@@ -156,20 +156,17 @@ const isAdmin = computed(() => {
|
||||
const exportCourse = async () => {
|
||||
try {
|
||||
const response = await fetch(
|
||||
'/api/method/lms.lms.api.export_course_as_zip',
|
||||
'/api/method/lms.lms.api.export_course_as_zip?course_name=' +
|
||||
course.data.name,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
course_name: course.data.name,
|
||||
}),
|
||||
method: 'GET',
|
||||
credentials: 'include',
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text()
|
||||
console.error('Error response:', errorText)
|
||||
throw new Error('Download failed')
|
||||
}
|
||||
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
class="h-[120px] flex items-center justify-center bg-surface-gray-1 border border-dashed border-outline-gray-3 rounded-md"
|
||||
>
|
||||
<div
|
||||
class="w-fit bg-surface-white border rounded-md p-2 flex items-center justify-between items-center space-x-4"
|
||||
class="w-fit bg-surface-white border rounded-md p-2 flex items-center justify-between items-center space-x-4 mx-5"
|
||||
>
|
||||
<div class="space-y-2">
|
||||
<div class="font-medium leading-5 text-ink-gray-9">
|
||||
|
||||
@@ -132,6 +132,7 @@ import ChildTable from '@/components/Controls/ChildTable.vue'
|
||||
|
||||
const show = defineModel()
|
||||
const exercises = defineModel<ProgrammingExercises>('exercises')
|
||||
const totalExercises = defineModel<number>('totalExercises')
|
||||
const isDirty = ref(false)
|
||||
const originalTestCaseCount = ref(0)
|
||||
|
||||
@@ -150,7 +151,6 @@ const languageOptions = [
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
exerciseID: string
|
||||
getExerciseCount: () => Promise<number>
|
||||
}>(),
|
||||
{
|
||||
exerciseID: 'new',
|
||||
@@ -257,7 +257,7 @@ const createNewExercise = (close: () => void) => {
|
||||
close()
|
||||
isDirty.value = false
|
||||
exercises.value?.reload()
|
||||
props.getExerciseCount()
|
||||
totalExercises.value.reload()
|
||||
toast.success(__('Programming Exercise created successfully'))
|
||||
},
|
||||
onError(err: any) {
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<div class="p-5">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ __('{0} Exercises').format(exerciseCount) }}
|
||||
{{ __('{0} Exercises').format(exercises.data?.length) }}
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-5">
|
||||
<FormControl
|
||||
@@ -69,9 +69,10 @@
|
||||
showForm = true
|
||||
},
|
||||
}"
|
||||
class="h-[79vh] border-b"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center rounded bg-surface-gray-2 p-2"
|
||||
class="mb-2 grid items-center rounded bg-surface-white border-b rounded-none p-2"
|
||||
>
|
||||
<ListHeaderItem :item="item" v-for="item in columns">
|
||||
<template #prefix="{ item }">
|
||||
@@ -115,20 +116,21 @@
|
||||
</ListView>
|
||||
</div>
|
||||
<EmptyState v-else type="Programming Exercises" />
|
||||
<div
|
||||
v-if="exercises.data && exercises.hasNextPage"
|
||||
class="flex justify-center my-5"
|
||||
>
|
||||
<Button @click="exercises.next()">
|
||||
<div class="flex items-center justify-end space-x-3 mt-3">
|
||||
<Button v-if="exercises.hasNextPage" @click="exercises.next()">
|
||||
{{ __('Load More') }}
|
||||
</Button>
|
||||
<div v-if="exercises.hasNextPage" class="h-8 border-l"></div>
|
||||
<div class="text-ink-gray-5">
|
||||
{{ exercises.data?.length }} {{ __('of') }} {{ totalExercises.data }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ProgrammingExerciseForm
|
||||
v-model="showForm"
|
||||
v-model:exercises="exercises"
|
||||
:exerciseID="exerciseID"
|
||||
:getExerciseCount="getExerciseCount"
|
||||
v-model:totalExercises="totalExercises"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
@@ -137,6 +139,7 @@ import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
call,
|
||||
createResource,
|
||||
createListResource,
|
||||
dayjs,
|
||||
FeatherIcon,
|
||||
@@ -156,7 +159,6 @@ import { sessionStore } from '@/stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
import ProgrammingExerciseForm from '@/pages/ProgrammingExercises/ProgrammingExerciseForm.vue'
|
||||
|
||||
const exerciseCount = ref<number>(0)
|
||||
const readOnlyMode = window.read_only_mode
|
||||
const { brand } = sessionStore()
|
||||
const showForm = ref<boolean>(false)
|
||||
@@ -170,7 +172,6 @@ const { $dialog } = app?.appContext.config.globalProperties
|
||||
|
||||
onMounted(() => {
|
||||
validatePermissions()
|
||||
getExerciseCount()
|
||||
})
|
||||
|
||||
const validatePermissions = () => {
|
||||
@@ -185,19 +186,6 @@ const validatePermissions = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getExerciseCount = (filters: any = {}) => {
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Programming Exercise',
|
||||
filters: filters,
|
||||
})
|
||||
.then((count: number) => {
|
||||
exerciseCount.value = count
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error('Error fetching exercise count:', error)
|
||||
})
|
||||
}
|
||||
|
||||
const exercises = createListResource({
|
||||
doctype: 'LMS Programming Exercise',
|
||||
cache: ['programmingExercises'],
|
||||
@@ -212,7 +200,9 @@ const updateList = () => {
|
||||
filters: filters,
|
||||
})
|
||||
exercises.reload()
|
||||
getExerciseCount(filters)
|
||||
totalExercises.update({
|
||||
filters: filters,
|
||||
})
|
||||
}
|
||||
|
||||
const getFilters = () => {
|
||||
@@ -266,6 +256,20 @@ const deleteExercises = (selections: Set<string>, unselectAll: () => void) => {
|
||||
unselectAll()
|
||||
}
|
||||
|
||||
const totalExercises = createResource({
|
||||
url: 'frappe.client.get_count',
|
||||
params: {
|
||||
doctype: 'LMS Programming Exercise',
|
||||
filters: getFilters(),
|
||||
},
|
||||
auto: true,
|
||||
cache: ['programming_exercises_count', user.data?.name],
|
||||
onError(err: any) {
|
||||
toast.error(err.messages?.[0] || err)
|
||||
console.error(err)
|
||||
},
|
||||
})
|
||||
|
||||
const languages = [
|
||||
{ label: ' ', value: ' ' },
|
||||
{ label: 'Python', value: 'Python' },
|
||||
@@ -277,13 +281,13 @@ const columns = computed(() => {
|
||||
{
|
||||
label: __('Title'),
|
||||
key: 'title',
|
||||
width: 3,
|
||||
width: 1,
|
||||
icon: 'file-text',
|
||||
},
|
||||
{
|
||||
label: __('Language'),
|
||||
key: 'language',
|
||||
width: 2,
|
||||
width: 1,
|
||||
align: 'left',
|
||||
icon: 'code',
|
||||
},
|
||||
@@ -292,6 +296,7 @@ const columns = computed(() => {
|
||||
key: 'modified',
|
||||
width: 1,
|
||||
icon: 'clock',
|
||||
align: 'right',
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
@@ -205,7 +205,7 @@ const quizzes = createListResource({
|
||||
return data.map((quiz) => {
|
||||
return {
|
||||
...quiz,
|
||||
modified: dayjs(quiz.modified).fromNow(true),
|
||||
modified: dayjs(quiz.modified).format('DD MMM YYYY'),
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -303,7 +303,7 @@ const quizColumns = computed(() => {
|
||||
label: __('Updated On'),
|
||||
key: 'modified',
|
||||
width: 1,
|
||||
align: 'center',
|
||||
align: 'right',
|
||||
icon: 'clock',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -403,8 +403,8 @@ export function getUserTimezone() {
|
||||
}
|
||||
}
|
||||
|
||||
export function getSidebarLinks() {
|
||||
let links = getSidebarItems()
|
||||
export function getSidebarLinks(forMobile = false) {
|
||||
let links = getSidebarItems(forMobile)
|
||||
|
||||
links.forEach((link) => {
|
||||
link.items = link.items.filter((item) => {
|
||||
@@ -419,7 +419,7 @@ export function getSidebarLinks() {
|
||||
return links
|
||||
}
|
||||
|
||||
const getSidebarItems = () => {
|
||||
const getSidebarItems = (forMobile = false) => {
|
||||
const { userResource } = usersStore()
|
||||
const { settings } = useSettings()
|
||||
|
||||
@@ -441,7 +441,7 @@ const getSidebarItems = () => {
|
||||
icon: 'Search',
|
||||
to: 'Search',
|
||||
condition: () => {
|
||||
return userResource?.data
|
||||
return !forMobile && userResource?.data
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -449,7 +449,7 @@ const getSidebarItems = () => {
|
||||
icon: 'Bell',
|
||||
to: 'Notifications',
|
||||
condition: () => {
|
||||
return userResource?.data
|
||||
return !forMobile && userResource?.data
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -476,7 +476,7 @@ const getSidebarItems = () => {
|
||||
activeFor: ['Programs', 'ProgramDetail'],
|
||||
await: true,
|
||||
condition: () => {
|
||||
return checkIfCanAddProgram()
|
||||
return checkIfCanAddProgram(forMobile)
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -514,7 +514,8 @@ const getSidebarItems = () => {
|
||||
: settings.data?.contact_us_email,
|
||||
condition: () => {
|
||||
return (
|
||||
(settings?.data?.contact_us_email &&
|
||||
(!forMobile &&
|
||||
settings?.data?.contact_us_email &&
|
||||
userResource?.data) ||
|
||||
settings?.data?.contact_us_url
|
||||
)
|
||||
@@ -531,7 +532,7 @@ const getSidebarItems = () => {
|
||||
icon: 'CircleHelp',
|
||||
to: 'Quizzes',
|
||||
condition: () => {
|
||||
return isAdmin()
|
||||
return !forMobile && isAdmin()
|
||||
},
|
||||
activeFor: [
|
||||
'Quizzes',
|
||||
@@ -546,7 +547,7 @@ const getSidebarItems = () => {
|
||||
icon: 'Pencil',
|
||||
to: 'Assignments',
|
||||
condition: () => {
|
||||
return isAdmin()
|
||||
return !forMobile && isAdmin()
|
||||
},
|
||||
activeFor: [
|
||||
'Assignments',
|
||||
@@ -559,7 +560,7 @@ const getSidebarItems = () => {
|
||||
icon: 'Code',
|
||||
to: 'ProgrammingExercises',
|
||||
condition: () => {
|
||||
return isAdmin()
|
||||
return !forMobile && isAdmin()
|
||||
},
|
||||
activeFor: [
|
||||
'ProgrammingExercises',
|
||||
@@ -581,10 +582,11 @@ const isAdmin = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const checkIfCanAddProgram = () => {
|
||||
const checkIfCanAddProgram = (forMobile = false) => {
|
||||
const { userResource } = usersStore()
|
||||
const { programs } = useSettings()
|
||||
if (!userResource.data) return false
|
||||
if (forMobile) return false
|
||||
if (userResource?.data?.is_moderator || userResource?.data?.is_instructor) {
|
||||
return true
|
||||
}
|
||||
|
||||
16
frontend/src/utils/theme.ts
Normal file
16
frontend/src/utils/theme.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import { ref } from 'vue'
|
||||
|
||||
const theme = ref<'light' | 'dark'>(localStorage.getItem('theme') as 'light' | 'dark' || 'light')
|
||||
|
||||
const toggleTheme = () => {
|
||||
const newTheme: 'light' | 'dark' = theme.value === 'dark' ? 'light' : 'dark'
|
||||
applyTheme(newTheme)
|
||||
}
|
||||
|
||||
const applyTheme = (value: 'light' | 'dark') => {
|
||||
document.documentElement.setAttribute('data-theme', value)
|
||||
localStorage.setItem('theme', value)
|
||||
theme.value = value
|
||||
}
|
||||
|
||||
export { applyTheme, toggleTheme, theme }
|
||||
@@ -456,6 +456,7 @@ def create_evaluator(zip_file):
|
||||
return
|
||||
|
||||
if not frappe.db.exists("User", evaluator_data["evaluator"]):
|
||||
evaluator_data["email"] = evaluator_data["evaluator"]
|
||||
create_user(evaluator_data)
|
||||
|
||||
if not frappe.db.exists("Course Evaluator", evaluator_data["name"]):
|
||||
|
||||
@@ -20,11 +20,12 @@
|
||||
"section_break_ydgh",
|
||||
"column_break_oqqy",
|
||||
"status",
|
||||
"question",
|
||||
"column_break_tbnv",
|
||||
"comments",
|
||||
"section_break_rqal",
|
||||
"question",
|
||||
"column_break_esgd",
|
||||
"course",
|
||||
"column_break_esgd",
|
||||
"lesson"
|
||||
],
|
||||
"fields": [
|
||||
@@ -145,13 +146,17 @@
|
||||
{
|
||||
"fieldname": "section_break_ydgh",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_tbnv",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2026-02-05 11:38:03.792865",
|
||||
"modified": "2026-04-06 18:24:11.837953",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"module": "LMS",
|
||||
"name": "LMS Assignment Submission",
|
||||
|
||||
@@ -35,10 +35,10 @@ class LMSCertificate(Document):
|
||||
custom_template = frappe.db.get_single_value("LMS Settings", "certification_template")
|
||||
|
||||
args = {
|
||||
"student_name": self.member_name,
|
||||
"member_name": self.member_name,
|
||||
"course_name": self.course,
|
||||
"course_title": frappe.db.get_value("LMS Course", self.course, "title"),
|
||||
"certificate_name": self.name,
|
||||
"name": self.name,
|
||||
"template": self.template,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import frappe
|
||||
|
||||
from lms.lms.utils import (
|
||||
complete_enrollment,
|
||||
get_lms_route,
|
||||
get_order_summary,
|
||||
)
|
||||
|
||||
|
||||
def get_payment_gateway():
|
||||
return frappe.db.get_single_value("LMS Settings", "payment_gateway")
|
||||
@@ -21,22 +27,25 @@ def validate_currency(payment_gateway, currency):
|
||||
def get_payment_link(
|
||||
doctype: str,
|
||||
docname: str,
|
||||
title: str,
|
||||
amount: float,
|
||||
discount_amount: float,
|
||||
gst_amount: float,
|
||||
currency: str,
|
||||
address: dict,
|
||||
redirect_to: str,
|
||||
payment_for_certificate: int,
|
||||
coupon_code: str = None,
|
||||
coupon: str = None,
|
||||
coupon_code: str,
|
||||
country: str,
|
||||
):
|
||||
payment_gateway = get_payment_gateway()
|
||||
address = frappe._dict(address)
|
||||
original_amount = amount
|
||||
amount -= discount_amount
|
||||
redirect_to = get_redirect_url(doctype, docname, payment_for_certificate)
|
||||
|
||||
details = frappe._dict(get_order_summary(doctype, docname, coupon=coupon_code, country=country))
|
||||
title = details.title
|
||||
currency = details.currency
|
||||
original_amount = details.original_amount
|
||||
discount_amount = details.get("discount_amount", 0)
|
||||
gst_amount = details.get("gst_applied", 0)
|
||||
amount = original_amount - discount_amount
|
||||
amount_with_gst = get_amount_with_gst(amount, gst_amount)
|
||||
coupon = details.get("coupon")
|
||||
total_amount = amount_with_gst if amount_with_gst else amount
|
||||
|
||||
payment = record_payment(
|
||||
address,
|
||||
@@ -51,10 +60,16 @@ def get_payment_link(
|
||||
coupon_code,
|
||||
coupon,
|
||||
)
|
||||
|
||||
if total_amount <= 0:
|
||||
frappe.db.set_value("LMS Payment", payment.name, "payment_received", 1)
|
||||
complete_enrollment(payment.name, doctype, docname)
|
||||
return redirect_to
|
||||
|
||||
controller = get_controller(payment_gateway)
|
||||
|
||||
payment_details = {
|
||||
"amount": amount_with_gst if amount_with_gst else amount,
|
||||
"amount": total_amount,
|
||||
"title": f"Payment for {doctype} {title} {docname}",
|
||||
"description": f"{address.billing_name}'s payment for {title}",
|
||||
"reference_doctype": doctype,
|
||||
@@ -99,8 +114,8 @@ def record_payment(
|
||||
amount_with_gst: float = 0,
|
||||
discount_amount: float = 0,
|
||||
payment_for_certificate: int = 0,
|
||||
coupon_code: str = None,
|
||||
coupon: str = None,
|
||||
coupon_code: str | None = None,
|
||||
coupon: str | None = None,
|
||||
):
|
||||
address = frappe._dict(address)
|
||||
address_name = save_address(address)
|
||||
@@ -138,6 +153,15 @@ def record_payment(
|
||||
return payment_doc
|
||||
|
||||
|
||||
def get_redirect_url(doctype: str, docname: str, payment_for_certificate: int) -> str:
|
||||
if int(payment_for_certificate):
|
||||
return get_lms_route(f"courses/{docname}/certification")
|
||||
elif doctype == "LMS Course":
|
||||
return get_lms_route(f"courses/{docname}")
|
||||
else:
|
||||
return get_lms_route(f"batches/{docname}")
|
||||
|
||||
|
||||
def save_address(address: dict) -> str:
|
||||
filters = {"email_id": frappe.session.user}
|
||||
exists = frappe.db.exists("Address", filters)
|
||||
|
||||
@@ -1907,17 +1907,21 @@ def update_payment_record(doctype: str, docname: str):
|
||||
if len(request):
|
||||
data = request[0].data
|
||||
data = frappe._dict(json.loads(data))
|
||||
payment_doc = get_payment_doc(data.payment)
|
||||
|
||||
update_payment_details(data)
|
||||
update_coupon_redemption(payment_doc)
|
||||
complete_enrollment(data.payment, doctype, docname)
|
||||
|
||||
if payment_doc.payment_for_certificate:
|
||||
update_certificate_purchase(docname, data.payment)
|
||||
elif doctype == "LMS Course":
|
||||
enroll_in_course(docname, data.payment)
|
||||
else:
|
||||
enroll_in_batch(docname, data.payment)
|
||||
|
||||
def complete_enrollment(payment_name: str, doctype: str, docname: str):
|
||||
payment_doc = get_payment_doc(payment_name)
|
||||
update_coupon_redemption(payment_doc)
|
||||
|
||||
if payment_doc.payment_for_certificate:
|
||||
update_certificate_purchase(docname, payment_name)
|
||||
elif doctype == "LMS Course":
|
||||
enroll_in_course(docname, payment_name)
|
||||
else:
|
||||
enroll_in_batch(docname, payment_name)
|
||||
|
||||
|
||||
def get_integration_requests(doctype: str, docname: str):
|
||||
|
||||
@@ -7,8 +7,8 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Learning VERSION\n"
|
||||
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
|
||||
"POT-Creation-Date: 2026-03-27 16:17+0000\n"
|
||||
"PO-Revision-Date: 2026-03-27 16:17+0000\n"
|
||||
"POT-Creation-Date: 2026-04-03 16:11+0000\n"
|
||||
"PO-Revision-Date: 2026-04-03 16:11+0000\n"
|
||||
"Last-Translator: jannat@frappe.io\n"
|
||||
"Language-Team: jannat@frappe.io\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
@@ -334,7 +334,7 @@ msgstr ""
|
||||
msgid "All Batches"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Courses/Courses.vue:54 lms/lms/widgets/BreadCrumb.html:3
|
||||
#: frontend/src/pages/Courses/Courses.vue:36 lms/lms/widgets/BreadCrumb.html:3
|
||||
msgid "All Courses"
|
||||
msgstr ""
|
||||
|
||||
@@ -926,11 +926,11 @@ msgstr ""
|
||||
msgid "Batch end date cannot be before the batch start date"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:194
|
||||
#: lms/lms/api.py:193
|
||||
msgid "Batch has already started."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:189
|
||||
#: lms/lms/api.py:188
|
||||
msgid "Batch is sold out."
|
||||
msgstr ""
|
||||
|
||||
@@ -1040,7 +1040,7 @@ msgstr ""
|
||||
msgid "Cancelled"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:2325
|
||||
#: lms/lms/api.py:2335
|
||||
msgid "Cannot search for roles: {0}"
|
||||
msgstr ""
|
||||
|
||||
@@ -1063,7 +1063,7 @@ msgstr ""
|
||||
#: frontend/src/pages/Batches/components/NewBatchModal.vue:51
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:38
|
||||
#: frontend/src/pages/Courses/CourseForm.vue:23
|
||||
#: frontend/src/pages/Courses/Courses.vue:74
|
||||
#: frontend/src/pages/Courses/Courses.vue:56
|
||||
#: frontend/src/pages/Courses/NewCourseModal.vue:21
|
||||
#: lms/lms/doctype/lms_batch/lms_batch.json
|
||||
#: lms/lms/doctype/lms_category/lms_category.json
|
||||
@@ -1079,7 +1079,7 @@ msgstr ""
|
||||
msgid "Category added successfully"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/utils/index.js:835
|
||||
#: frontend/src/utils/index.js:837
|
||||
msgid "Category created successfully"
|
||||
msgstr ""
|
||||
|
||||
@@ -1140,7 +1140,7 @@ msgstr ""
|
||||
#: frontend/src/pages/Batches/Batches.vue:85
|
||||
#: frontend/src/pages/Courses/CourseCertification.vue:10
|
||||
#: frontend/src/pages/Courses/CourseCertification.vue:135
|
||||
#: frontend/src/pages/Courses/Courses.vue:84 lms/fixtures/custom_field.json
|
||||
#: frontend/src/pages/Courses/Courses.vue:66 lms/fixtures/custom_field.json
|
||||
#: lms/lms/doctype/certification/certification.json
|
||||
#: lms/lms/doctype/lms_batch/lms_batch.json
|
||||
#: lms/lms/doctype/lms_enrollment/lms_enrollment.json
|
||||
@@ -1667,6 +1667,10 @@ msgstr ""
|
||||
msgid "Correct Answer"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/course_import_export.py:196
|
||||
msgid "Could not create the course ZIP file. Please try again later. Error: {0}"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the country (Link) field in DocType 'User'
|
||||
#. Label of the country (Link) field in DocType 'Job Opportunity'
|
||||
#. Label of the country (Link) field in DocType 'Payment Country'
|
||||
@@ -1941,9 +1945,9 @@ msgstr ""
|
||||
#: frontend/src/pages/Batches/components/BatchDashboard.vue:20
|
||||
#: frontend/src/pages/Batches/components/BatchOverlay.vue:45
|
||||
#: frontend/src/pages/Courses/CourseCertification.vue:127
|
||||
#: frontend/src/pages/Courses/CourseDetail.vue:143
|
||||
#: frontend/src/pages/Courses/Courses.vue:356
|
||||
#: frontend/src/pages/Courses/Courses.vue:363 frontend/src/pages/Lesson.vue:564
|
||||
#: frontend/src/pages/Courses/CourseDetail.vue:227
|
||||
#: frontend/src/pages/Courses/Courses.vue:374
|
||||
#: frontend/src/pages/Courses/Courses.vue:381 frontend/src/pages/Lesson.vue:564
|
||||
#: frontend/src/pages/LessonForm.vue:475
|
||||
#: frontend/src/pages/Programs/ProgramForm.vue:49
|
||||
#: frontend/src/pages/Programs/Programs.vue:35
|
||||
@@ -1984,7 +1988,7 @@ msgstr ""
|
||||
#: frontend/src/components/Modals/ChapterModal.vue:9
|
||||
#: frontend/src/pages/Assignments.vue:19
|
||||
#: frontend/src/pages/Batches/Batches.vue:33
|
||||
#: frontend/src/pages/Courses/Courses.vue:36
|
||||
#: frontend/src/pages/Courses/Courses.vue:18
|
||||
#: frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue:33
|
||||
#: frontend/src/pages/Quizzes.vue:10
|
||||
msgid "Create"
|
||||
@@ -2061,7 +2065,7 @@ msgid "Create your first quiz"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Assignments.vue:175
|
||||
#: frontend/src/pages/Courses/Courses.vue:346
|
||||
#: frontend/src/pages/Courses/Courses.vue:335
|
||||
msgid "Created"
|
||||
msgstr ""
|
||||
|
||||
@@ -2129,7 +2133,7 @@ msgid "Cyan"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the show_dashboard (Check) field in DocType 'LMS Settings'
|
||||
#: frontend/src/pages/Courses/CourseDetail.vue:102
|
||||
#: frontend/src/pages/Courses/CourseDetail.vue:116
|
||||
#: lms/lms/doctype/lms_settings/lms_settings.json
|
||||
msgid "Dashboard"
|
||||
msgstr ""
|
||||
@@ -2204,6 +2208,7 @@ msgstr ""
|
||||
#: frontend/src/components/Settings/Badges.vue:171
|
||||
#: frontend/src/components/Settings/Coupons/CouponList.vue:133
|
||||
#: frontend/src/pages/Batches/BatchForm.vue:507
|
||||
#: frontend/src/pages/Courses/CourseDetail.vue:216
|
||||
#: frontend/src/pages/Courses/CourseForm.vue:544
|
||||
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseForm.vue:71
|
||||
#: frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue:240
|
||||
@@ -2256,7 +2261,7 @@ msgstr ""
|
||||
msgid "Deleting this lesson will permanently remove it from the course. This action cannot be undone. Are you sure you want to continue?"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:757
|
||||
#: lms/lms/api.py:756
|
||||
msgid "Deletion not allowed for {0}"
|
||||
msgstr ""
|
||||
|
||||
@@ -2307,6 +2312,10 @@ msgstr ""
|
||||
msgid "Details cannot be empty."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Courses/CourseImportModal.vue:33
|
||||
msgid "Device"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the disable_pwa (Check) field in DocType 'LMS Settings'
|
||||
#: lms/lms/doctype/lms_settings/lms_settings.json
|
||||
msgid "Disable PWA"
|
||||
@@ -2392,6 +2401,10 @@ msgstr ""
|
||||
msgid "Don’t miss this opportunity to enhance your skills. Click below to complete your enrollment"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Courses/CourseImportModal.vue:28
|
||||
msgid "Drag and drop a ZIP file, or upload from your"
|
||||
msgstr ""
|
||||
|
||||
#. Label of the dream_companies (Data) field in DocType 'User'
|
||||
#: lms/fixtures/custom_field.json
|
||||
msgid "Dream Companies"
|
||||
@@ -2666,7 +2679,7 @@ msgstr ""
|
||||
#: frontend/src/pages/Batches/Batches.vue:328
|
||||
#: frontend/src/pages/Batches/components/AdminBatchDashboard.vue:5
|
||||
#: frontend/src/pages/Courses/CourseDashboard.vue:5
|
||||
#: frontend/src/pages/Courses/Courses.vue:349
|
||||
#: frontend/src/pages/Courses/Courses.vue:338
|
||||
#: frontend/src/pages/Programs/StudentPrograms.vue:96
|
||||
msgid "Enrolled"
|
||||
msgstr ""
|
||||
@@ -2781,6 +2794,10 @@ msgstr ""
|
||||
msgid "Error deleting payment gateways"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/course_import_export.py:298
|
||||
msgid "Error downloading file"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Settings/GoogleMeetAccountModal.vue:194
|
||||
msgid "Error updating Google Meet Account"
|
||||
msgstr ""
|
||||
@@ -2875,7 +2892,7 @@ msgstr ""
|
||||
msgid "Evaluator added successfully"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Settings/Evaluators.vue:190
|
||||
#: frontend/src/components/Settings/Evaluators.vue:192
|
||||
msgid "Evaluator deleted successfully"
|
||||
msgstr ""
|
||||
|
||||
@@ -2970,6 +2987,10 @@ msgstr ""
|
||||
msgid "Explore More"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Courses/CourseDetail.vue:209
|
||||
msgid "Export"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Status' (Select) field in DocType 'LMS Assignment
|
||||
#. Submission'
|
||||
#. Option for the 'Status' (Select) field in DocType 'LMS Certificate
|
||||
@@ -3021,7 +3042,7 @@ msgstr ""
|
||||
msgid "Failed to update badge assignment: "
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/utils/index.js:899
|
||||
#: frontend/src/utils/index.js:901
|
||||
msgid "Failed to update meta tags {0}"
|
||||
msgstr ""
|
||||
|
||||
@@ -3059,6 +3080,15 @@ msgstr ""
|
||||
msgid "File Type"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/course_import_export.py:286
|
||||
msgid "File not found"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Courses/CourseImportModal.vue:150
|
||||
#: frontend/src/pages/Courses/CourseImportModal.vue:166
|
||||
msgid "File upload failed. Please try again."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/AssessmentPlugin.vue:55
|
||||
msgid "Filter assignments by course"
|
||||
msgstr ""
|
||||
@@ -3543,6 +3573,7 @@ msgid "Image: Corrupted Data Stream"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Sidebar/Configuration.vue:36
|
||||
#: frontend/src/pages/Courses/CourseImportModal.vue:83
|
||||
msgid "Import"
|
||||
msgstr ""
|
||||
|
||||
@@ -3550,8 +3581,16 @@ msgstr ""
|
||||
msgid "Import Batch"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Courses/Courses.vue:20
|
||||
msgid "Import Course"
|
||||
#: frontend/src/pages/Courses/CourseImportModal.vue:5
|
||||
msgid "Import Course from ZIP"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Courses/Courses.vue:353
|
||||
msgid "Import via Data Import Tool"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Courses/Courses.vue:363
|
||||
msgid "Import via ZIP"
|
||||
msgstr ""
|
||||
|
||||
#. Option for the 'Status' (Select) field in DocType 'LMS Certificate
|
||||
@@ -3680,10 +3719,26 @@ msgstr ""
|
||||
msgid "Invalid Quiz ID in content"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:760
|
||||
#: lms/lms/course_import_export.py:770
|
||||
msgid "Invalid ZIP file"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/course_import_export.py:339
|
||||
msgid "Invalid course ZIP: Missing course.json"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:1010
|
||||
msgid "Invalid course or chapter name"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:759
|
||||
msgid "Invalid document name"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:1017
|
||||
msgid "Invalid file path in package"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Sidebar/AppSidebar.vue:515
|
||||
msgid "Invite your team and students"
|
||||
msgstr ""
|
||||
@@ -4266,7 +4321,7 @@ msgstr ""
|
||||
msgid "LinkedIn ID"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Courses/Courses.vue:329
|
||||
#: frontend/src/pages/Courses/Courses.vue:318
|
||||
msgid "Live"
|
||||
msgstr ""
|
||||
|
||||
@@ -4291,7 +4346,7 @@ msgstr ""
|
||||
#: frontend/src/pages/Batches/components/AdminBatchDashboard.vue:119
|
||||
#: frontend/src/pages/CertifiedParticipants.vue:118
|
||||
#: frontend/src/pages/Courses/CourseDashboard.vue:119
|
||||
#: frontend/src/pages/Courses/Courses.vue:107
|
||||
#: frontend/src/pages/Courses/Courses.vue:89
|
||||
#: frontend/src/pages/JobApplications.vue:101
|
||||
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmissions.vue:133
|
||||
#: frontend/src/pages/ProgrammingExercises/ProgrammingExercises.vue:123
|
||||
@@ -4397,7 +4452,7 @@ msgstr ""
|
||||
msgid "Mark"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Notifications.vue:12
|
||||
#: frontend/src/pages/Notifications.vue:14
|
||||
msgid "Mark all as read"
|
||||
msgstr ""
|
||||
|
||||
@@ -4732,7 +4787,7 @@ msgstr ""
|
||||
msgid "Meta Tags"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:1542
|
||||
#: lms/lms/api.py:1552
|
||||
msgid "Meta tags should be a list."
|
||||
msgstr ""
|
||||
|
||||
@@ -4792,11 +4847,11 @@ msgstr ""
|
||||
msgid "Modified By"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:171
|
||||
#: lms/lms/api.py:170
|
||||
msgid "Module Name is incorrect or does not exist."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:167
|
||||
#: lms/lms/api.py:166
|
||||
msgid "Module is incorrect."
|
||||
msgstr ""
|
||||
|
||||
@@ -4861,7 +4916,7 @@ msgstr ""
|
||||
#: frontend/src/components/Settings/Members.vue:17
|
||||
#: frontend/src/components/Settings/PaymentGateways.vue:16
|
||||
#: frontend/src/components/Settings/ZoomSettings.vue:17
|
||||
#: frontend/src/pages/Courses/Courses.vue:333
|
||||
#: frontend/src/pages/Courses/Courses.vue:322
|
||||
#: frontend/src/pages/Programs/Programs.vue:10
|
||||
#: lms/lms/doctype/lms_badge/lms_badge.json
|
||||
msgid "New"
|
||||
@@ -4869,7 +4924,7 @@ msgstr ""
|
||||
|
||||
#: frontend/src/pages/Batches/Batches.vue:10
|
||||
#: frontend/src/pages/Batches/components/NewBatchModal.vue:5
|
||||
#: frontend/src/pages/Notifications.vue:90 lms/www/_lms.py:154
|
||||
#: frontend/src/pages/Notifications.vue:95 lms/www/_lms.py:154
|
||||
msgid "New Batch"
|
||||
msgstr ""
|
||||
|
||||
@@ -4877,9 +4932,9 @@ msgstr ""
|
||||
msgid "New Coupon"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Courses/Courses.vue:13
|
||||
#: frontend/src/pages/Courses/Courses.vue:346
|
||||
#: frontend/src/pages/Courses/NewCourseModal.vue:5
|
||||
#: frontend/src/pages/Notifications.vue:89 lms/www/_lms.py:98
|
||||
#: frontend/src/pages/Notifications.vue:94 lms/www/_lms.py:98
|
||||
msgid "New Course"
|
||||
msgstr ""
|
||||
|
||||
@@ -4924,11 +4979,11 @@ msgstr ""
|
||||
msgid "New Zoom Account"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:427
|
||||
#: lms/lms/utils.py:471
|
||||
msgid "New comment in batch {0}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:418
|
||||
#: lms/lms/utils.py:462
|
||||
msgid "New reply on the topic {0} in course {1}"
|
||||
msgstr ""
|
||||
|
||||
@@ -5045,6 +5100,10 @@ msgstr ""
|
||||
msgid "No quizzes added yet."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Notifications.vue:153
|
||||
msgid "No read notifications"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Controls/Autocomplete.vue:136
|
||||
#: frontend/src/components/Controls/MultiSelect.vue:77
|
||||
#: frontend/src/pages/Search/Search.vue:47
|
||||
@@ -5067,6 +5126,10 @@ msgstr ""
|
||||
msgid "No submissions"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Notifications.vue:152
|
||||
msgid "No unread notifications"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/EmptyState.vue:5 lms/templates/course_list.html:13
|
||||
msgid "No {0}"
|
||||
msgstr ""
|
||||
@@ -5122,10 +5185,6 @@ msgstr ""
|
||||
msgid "Notes"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Notifications.vue:143
|
||||
msgid "Nothing to see here."
|
||||
msgstr ""
|
||||
|
||||
#. Label of the notification_sent (Check) field in DocType 'LMS Batch'
|
||||
#. Label of the notification_sent (Check) field in DocType 'LMS Course'
|
||||
#: lms/lms/doctype/lms_batch/lms_batch.json
|
||||
@@ -5140,6 +5199,10 @@ msgstr ""
|
||||
msgid "Notifications"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Notifications.vue:160
|
||||
msgid "Notifications you have read will appear here."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/widgets/NoPreviewModal.html:30
|
||||
msgid "Notify me when available"
|
||||
msgstr ""
|
||||
@@ -5197,6 +5260,10 @@ msgstr ""
|
||||
msgid "Only PDF files are allowed."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/utils/index.js:661
|
||||
msgid "Only ZIP files are allowed."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/utils/index.js:658
|
||||
msgid "Only document file of type .doc or .docx are allowed."
|
||||
msgstr ""
|
||||
@@ -5213,7 +5280,7 @@ msgstr ""
|
||||
msgid "Only show batches that offer a certificate"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Courses/Courses.vue:80
|
||||
#: frontend/src/pages/Courses/Courses.vue:62
|
||||
msgid "Only show courses that offer a certificate"
|
||||
msgstr ""
|
||||
|
||||
@@ -5221,7 +5288,7 @@ msgstr ""
|
||||
msgid "Only zip files are allowed"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/utils/index.js:664
|
||||
#: frontend/src/utils/index.js:666
|
||||
msgid "Only {0} file is allowed."
|
||||
msgstr ""
|
||||
|
||||
@@ -5339,7 +5406,7 @@ msgstr ""
|
||||
msgid "Oversee all users, content, and system settings"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Courses/CourseDetail.vue:97
|
||||
#: frontend/src/pages/Courses/CourseDetail.vue:111
|
||||
msgid "Overview"
|
||||
msgstr ""
|
||||
|
||||
@@ -5594,11 +5661,11 @@ msgstr ""
|
||||
msgid "Please add <a href='{0}'>{1}</a> for <a href='{2}'>{3}</a> to send calendar invites for evaluations."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/user.py:75
|
||||
#: lms/lms/user.py:77
|
||||
msgid "Please ask your administrator to verify your sign-up"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/user.py:73
|
||||
#: lms/lms/user.py:75
|
||||
msgid "Please check your email for verification"
|
||||
msgstr ""
|
||||
|
||||
@@ -5678,15 +5745,15 @@ msgstr ""
|
||||
msgid "Please login to access the quiz."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:163
|
||||
#: lms/lms/api.py:162
|
||||
msgid "Please login to continue with payment."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:2038
|
||||
#: lms/lms/utils.py:2081
|
||||
msgid "Please login to view program details."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:2003
|
||||
#: lms/lms/utils.py:2046
|
||||
msgid "Please login to view programs."
|
||||
msgstr ""
|
||||
|
||||
@@ -6455,7 +6522,7 @@ msgstr ""
|
||||
msgid "SEO"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/utils/index.js:681
|
||||
#: frontend/src/utils/index.js:683
|
||||
msgid "SVG contains potentially unsafe content."
|
||||
msgstr ""
|
||||
|
||||
@@ -6486,7 +6553,7 @@ msgstr ""
|
||||
#: frontend/src/components/Settings/Transactions/TransactionDetails.vue:29
|
||||
#: frontend/src/pages/Batches/BatchDetail.vue:17
|
||||
#: frontend/src/pages/Batches/components/NewBatchModal.vue:107
|
||||
#: frontend/src/pages/Courses/CourseDetail.vue:17
|
||||
#: frontend/src/pages/Courses/CourseDetail.vue:21
|
||||
#: frontend/src/pages/Courses/NewCourseModal.vue:69
|
||||
#: frontend/src/pages/JobForm.vue:12 frontend/src/pages/LessonForm.vue:14
|
||||
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseForm.vue:107
|
||||
@@ -6533,7 +6600,7 @@ msgstr ""
|
||||
|
||||
#: frontend/src/components/Settings/Evaluators.vue:57
|
||||
#: frontend/src/components/Settings/Members.vue:25
|
||||
#: frontend/src/pages/Courses/Courses.vue:64 frontend/src/pages/Jobs.vue:59
|
||||
#: frontend/src/pages/Courses/Courses.vue:46 frontend/src/pages/Jobs.vue:59
|
||||
#: frontend/src/pages/Search/Search.vue:5
|
||||
#: frontend/src/pages/Search/Search.vue:250
|
||||
msgid "Search"
|
||||
@@ -6716,7 +6783,7 @@ msgstr ""
|
||||
|
||||
#: frontend/src/components/Settings/Settings.vue:9
|
||||
#: frontend/src/components/Sidebar/AppSidebar.vue:640
|
||||
#: frontend/src/pages/Courses/CourseDetail.vue:107
|
||||
#: frontend/src/pages/Courses/CourseDetail.vue:121
|
||||
#: frontend/src/pages/ProfileRoles.vue:4
|
||||
#: frontend/src/pages/ProgrammingExercises/ProgrammingExerciseSubmission.vue:19
|
||||
#: frontend/src/pages/QuizForm.vue:86
|
||||
@@ -7101,7 +7168,7 @@ msgstr ""
|
||||
msgid "Sunday"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:1033
|
||||
#: lms/lms/api.py:1043
|
||||
msgid "Suspicious pattern found in {0}: {1}"
|
||||
msgstr ""
|
||||
|
||||
@@ -7189,7 +7256,7 @@ msgstr ""
|
||||
msgid "Template"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/user.py:40
|
||||
#: lms/lms/user.py:42
|
||||
msgid "Temporarily Disabled"
|
||||
msgstr ""
|
||||
|
||||
@@ -7237,7 +7304,7 @@ msgstr ""
|
||||
msgid "The Google Meet account does not have a Google Calendar configured. Please set up a Google Calendar first."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:2274
|
||||
#: lms/lms/utils.py:2317
|
||||
msgid "The batch does not exist."
|
||||
msgstr ""
|
||||
|
||||
@@ -7245,7 +7312,7 @@ msgstr ""
|
||||
msgid "The batch you have enrolled for is starting tomorrow. Please be prepared and be on time for the session."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1757
|
||||
#: lms/lms/utils.py:1800
|
||||
msgid "The coupon code '{0}' is invalid."
|
||||
msgstr ""
|
||||
|
||||
@@ -7269,7 +7336,7 @@ msgstr ""
|
||||
msgid "The last day to schedule your evaluations is "
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:2258
|
||||
#: lms/lms/utils.py:2301
|
||||
msgid "The lesson does not exist."
|
||||
msgstr ""
|
||||
|
||||
@@ -7285,7 +7352,7 @@ msgstr ""
|
||||
msgid "The slot is already booked by another participant."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1455 lms/lms/utils.py:1955
|
||||
#: lms/lms/utils.py:1498 lms/lms/utils.py:1998
|
||||
msgid "The specified batch does not exist."
|
||||
msgstr ""
|
||||
|
||||
@@ -7332,6 +7399,10 @@ msgstr ""
|
||||
msgid "This badge has not been assigned to any students yet"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/doctype/lms_enrollment/lms_enrollment.py:56
|
||||
msgid "This batch is not associated with this course."
|
||||
msgstr ""
|
||||
|
||||
#. Label of the expire (Check) field in DocType 'Certification'
|
||||
#: lms/lms/doctype/certification/certification.json
|
||||
msgid "This certificate does no expire"
|
||||
@@ -7343,15 +7414,15 @@ msgstr ""
|
||||
msgid "This class has ended"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1786
|
||||
#: lms/lms/utils.py:1829
|
||||
msgid "This coupon has expired."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1789
|
||||
#: lms/lms/utils.py:1832
|
||||
msgid "This coupon has reached its maximum usage limit."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1798
|
||||
#: lms/lms/utils.py:1841
|
||||
msgid "This coupon is not applicable to this {0}."
|
||||
msgstr ""
|
||||
|
||||
@@ -7359,7 +7430,7 @@ msgstr ""
|
||||
msgid "This course has:"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1717
|
||||
#: lms/lms/utils.py:1760
|
||||
msgid "This course is free."
|
||||
msgstr ""
|
||||
|
||||
@@ -7566,11 +7637,11 @@ msgstr ""
|
||||
msgid "To Date"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1731
|
||||
#: lms/lms/utils.py:1774
|
||||
msgid "To join this batch, please contact the Administrator."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/user.py:41
|
||||
#: lms/lms/user.py:43
|
||||
msgid "Too many users signed up recently, so the registration is disabled. Please try back in an hour"
|
||||
msgstr ""
|
||||
|
||||
@@ -7690,7 +7761,7 @@ msgstr ""
|
||||
msgid "Unable to add member"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/utils/index.js:840
|
||||
#: frontend/src/utils/index.js:842
|
||||
msgid "Unable to create category"
|
||||
msgstr ""
|
||||
|
||||
@@ -7718,10 +7789,14 @@ msgid "Under Review"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Batches/Batches.vue:326
|
||||
#: frontend/src/pages/Courses/Courses.vue:347
|
||||
#: frontend/src/pages/Courses/Courses.vue:336
|
||||
msgid "Unpublished"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/course_import_export.py:773
|
||||
msgid "Unsafe file path detected"
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/components/Modals/EditCoverImage.vue:60
|
||||
#: frontend/src/components/UnsplashImageBrowser.vue:54
|
||||
msgid "Unsplash"
|
||||
@@ -7741,7 +7816,7 @@ msgstr ""
|
||||
#. Label of the upcoming (Check) field in DocType 'LMS Course'
|
||||
#: frontend/src/pages/Batches/Batches.vue:324
|
||||
#: frontend/src/pages/Courses/CourseForm.vue:129
|
||||
#: frontend/src/pages/Courses/Courses.vue:337
|
||||
#: frontend/src/pages/Courses/Courses.vue:326
|
||||
#: lms/lms/doctype/lms_certificate_request/lms_certificate_request.json
|
||||
#: lms/lms/doctype/lms_course/lms_course.json
|
||||
msgid "Upcoming"
|
||||
@@ -7849,7 +7924,7 @@ msgstr ""
|
||||
msgid "User Skill"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:1783
|
||||
#: lms/lms/api.py:1793
|
||||
msgid "User does not have permission to access this user's profile details."
|
||||
msgstr ""
|
||||
|
||||
@@ -8053,39 +8128,39 @@ msgstr ""
|
||||
msgid "You are already certified for this course. Click on the card below to open your certificate."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:183
|
||||
#: lms/lms/api.py:182
|
||||
msgid "You are already enrolled for this batch."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:177
|
||||
#: lms/lms/api.py:176
|
||||
msgid "You are already enrolled for this course."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1251
|
||||
#: lms/lms/utils.py:1294
|
||||
msgid "You are not authorized to view the assessments of this batch."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1453
|
||||
#: lms/lms/utils.py:1496
|
||||
msgid "You are not authorized to view the chart data of this batch."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:2045
|
||||
#: lms/lms/utils.py:2088
|
||||
msgid "You are not authorized to view the details of this program."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1663
|
||||
#: lms/lms/utils.py:1706
|
||||
msgid "You are not authorized to view the discussion replies for this topic."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1616
|
||||
#: lms/lms/utils.py:1659
|
||||
msgid "You are not authorized to view the discussion topics for this item."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1217
|
||||
#: lms/lms/utils.py:1260
|
||||
msgid "You are not authorized to view the question details."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:1362
|
||||
#: lms/lms/utils.py:1405
|
||||
msgid "You are not authorized to view the students of this batch."
|
||||
msgstr ""
|
||||
|
||||
@@ -8130,11 +8205,11 @@ msgstr ""
|
||||
msgid "You cannot change the roles in read-only mode."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/doctype/lms_enrollment/lms_enrollment.py:59
|
||||
#: lms/lms/doctype/lms_enrollment/lms_enrollment.py:64
|
||||
msgid "You cannot enroll in an unpublished course."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:2106
|
||||
#: lms/lms/utils.py:2149
|
||||
msgid "You cannot enroll in an unpublished program."
|
||||
msgstr ""
|
||||
|
||||
@@ -8150,35 +8225,35 @@ msgstr ""
|
||||
msgid "You cannot schedule evaluations for past slots."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:2286
|
||||
#: lms/lms/utils.py:2329
|
||||
msgid "You do not have access to this batch."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:2269
|
||||
#: lms/lms/utils.py:2312
|
||||
msgid "You do not have access to this course."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:855 lms/lms/doctype/lms_batch/lms_batch.py:365
|
||||
#: lms/lms/api.py:854 lms/lms/doctype/lms_batch/lms_batch.py:365
|
||||
msgid "You do not have permission to access announcements for this batch."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:2282
|
||||
#: lms/lms/api.py:2292
|
||||
msgid "You do not have permission to access badges."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:1145
|
||||
#: lms/lms/api.py:1155
|
||||
msgid "You do not have permission to access heatmap data."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:2121
|
||||
#: lms/lms/api.py:2131
|
||||
msgid "You do not have permission to access lesson completion stats."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:2161
|
||||
#: lms/lms/api.py:2171
|
||||
msgid "You do not have permission to access this course's assessment data."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:1684
|
||||
#: lms/lms/api.py:1694
|
||||
msgid "You do not have permission to access this course's progress data."
|
||||
msgstr ""
|
||||
|
||||
@@ -8186,7 +8261,7 @@ msgstr ""
|
||||
msgid "You do not have permission to access this page."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:1362 lms/lms/api.py:1371
|
||||
#: lms/lms/api.py:1372 lms/lms/api.py:1381
|
||||
msgid "You do not have permission to cancel this evaluation."
|
||||
msgstr ""
|
||||
|
||||
@@ -8194,31 +8269,35 @@ msgstr ""
|
||||
msgid "You do not have permission to create a live class."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:925
|
||||
#: lms/lms/api.py:924
|
||||
msgid "You do not have permission to delete this batch."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:1099
|
||||
#: lms/lms/api.py:1109
|
||||
msgid "You do not have permission to delete this chapter."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:885
|
||||
#: lms/lms/api.py:884
|
||||
msgid "You do not have permission to delete this course."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:514
|
||||
#: lms/lms/api.py:513
|
||||
msgid "You do not have permission to delete this lesson."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:597 lms/lms/api.py:973
|
||||
#: lms/lms/api.py:2372
|
||||
msgid "You do not have permission to export this course."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:596 lms/lms/api.py:972
|
||||
msgid "You do not have permission to modify this chapter."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:534
|
||||
#: lms/lms/api.py:533
|
||||
msgid "You do not have permission to modify this lesson."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:1431
|
||||
#: lms/lms/api.py:1441
|
||||
msgid "You do not have permission to modify this role."
|
||||
msgstr ""
|
||||
|
||||
@@ -8230,11 +8309,11 @@ msgstr ""
|
||||
msgid "You do not have permission to set up calendar events for this evaluation."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:1567 lms/lms/api.py:1571
|
||||
#: lms/lms/api.py:1577 lms/lms/api.py:1581
|
||||
msgid "You do not have permission to update meta tags."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:1608
|
||||
#: lms/lms/api.py:1618
|
||||
msgid "You do not have permission to update this submission."
|
||||
msgstr ""
|
||||
|
||||
@@ -8259,7 +8338,7 @@ msgstr ""
|
||||
msgid "You have already exceeded the maximum number of attempts allowed for this quiz."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:207
|
||||
#: lms/lms/api.py:206
|
||||
msgid "You have already purchased the certificate for this course."
|
||||
msgstr ""
|
||||
|
||||
@@ -8323,7 +8402,7 @@ msgstr ""
|
||||
msgid "You must be enrolled in the course to submit a review"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/doctype/lms_enrollment/lms_enrollment.py:73
|
||||
#: lms/lms/doctype/lms_enrollment/lms_enrollment.py:78
|
||||
msgid "You need to complete the payment for this course before enrolling."
|
||||
msgstr ""
|
||||
|
||||
@@ -8344,6 +8423,10 @@ msgstr ""
|
||||
msgid "You will have to get {0}% correct answers in order to pass the quiz."
|
||||
msgstr ""
|
||||
|
||||
#: frontend/src/pages/Notifications.vue:159
|
||||
msgid "You're all caught up! Check back later for updates."
|
||||
msgstr ""
|
||||
|
||||
#: lms/templates/emails/mentor_request_creation_email.html:4
|
||||
msgid "You've applied to become a mentor for this course. Your request is currently under review."
|
||||
msgstr ""
|
||||
@@ -8666,7 +8749,7 @@ msgstr ""
|
||||
msgid "{0} Quizzes"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:776 lms/lms/api.py:784
|
||||
#: lms/lms/api.py:775 lms/lms/api.py:783
|
||||
msgid "{0} Settings not found"
|
||||
msgstr ""
|
||||
|
||||
@@ -8710,7 +8793,7 @@ msgstr ""
|
||||
msgid "{0} is your evaluator"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:520
|
||||
#: lms/lms/utils.py:564
|
||||
msgid "{0} mentioned you in a comment"
|
||||
msgstr ""
|
||||
|
||||
@@ -8718,11 +8801,11 @@ msgstr ""
|
||||
msgid "{0} mentioned you in a comment in your batch."
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/utils.py:473 lms/lms/utils.py:479
|
||||
#: lms/lms/utils.py:517 lms/lms/utils.py:523
|
||||
msgid "{0} mentioned you in a comment in {1}"
|
||||
msgstr ""
|
||||
|
||||
#: lms/lms/api.py:838
|
||||
#: lms/lms/api.py:837
|
||||
msgid "{0} not found"
|
||||
msgstr ""
|
||||
|
||||
|
||||
@@ -16,41 +16,49 @@ class SCORMRenderer(BaseRenderer):
|
||||
def can_render(self):
|
||||
return "scorm/" in self.path
|
||||
|
||||
def _is_safe_path(self, path):
|
||||
scorm_root = os.path.realpath(os.path.join(frappe.local.site_path, "public", "scorm"))
|
||||
resolved = os.path.realpath(path)
|
||||
return resolved.startswith(scorm_root + os.sep) or resolved == scorm_root
|
||||
|
||||
def _serve_file(self, path):
|
||||
f = open(path, "rb")
|
||||
response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True)
|
||||
response.mimetype = mimetypes.guess_type(path)[0]
|
||||
return response
|
||||
|
||||
def render(self):
|
||||
path = os.path.join(frappe.local.site_path, "public", self.path.lstrip("/"))
|
||||
|
||||
if not self._is_safe_path(path):
|
||||
raise frappe.PermissionError
|
||||
|
||||
extension = os.path.splitext(path)[1]
|
||||
if not extension:
|
||||
path = f"{path}.html"
|
||||
|
||||
# check if path exists and is actually a file and not a folder
|
||||
if os.path.exists(path) and os.path.isfile(path):
|
||||
f = open(path, "rb")
|
||||
response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True)
|
||||
response.mimetype = mimetypes.guess_type(path)[0]
|
||||
return response
|
||||
return self._serve_file(path)
|
||||
else:
|
||||
path = path.replace(".html", "")
|
||||
if os.path.exists(path) and os.path.isdir(path):
|
||||
index_path = os.path.join(path, "index.html")
|
||||
if os.path.exists(index_path):
|
||||
f = open(index_path, "rb")
|
||||
response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True)
|
||||
response.mimetype = mimetypes.guess_type(index_path)[0]
|
||||
return response
|
||||
return self._serve_file(index_path)
|
||||
elif not os.path.exists(path):
|
||||
chapter_folder = "/".join(self.path.split("/")[:3])
|
||||
chapter_folder_path = os.path.realpath(frappe.get_site_path("public", chapter_folder))
|
||||
file = path.split("/")[-1]
|
||||
correct_file_path = None
|
||||
|
||||
if not self._is_safe_path(chapter_folder_path):
|
||||
raise frappe.PermissionError
|
||||
|
||||
for root, _dirs, files in os.walk(chapter_folder_path):
|
||||
if file in files:
|
||||
correct_file_path = os.path.join(root, file)
|
||||
break
|
||||
|
||||
if correct_file_path:
|
||||
f = open(correct_file_path, "rb")
|
||||
response = Response(wrap_file(frappe.local.request.environ, f), direct_passthrough=True)
|
||||
response.mimetype = mimetypes.guess_type(correct_file_path)[0]
|
||||
return response
|
||||
if correct_file_path and self._is_safe_path(correct_file_path):
|
||||
return self._serve_file(correct_file_path)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<p>
|
||||
{{ _("Dear ") }} {{ student_name }},
|
||||
{{ _("Dear ") }} {{ member_name }},
|
||||
</p>
|
||||
<br>
|
||||
<p>
|
||||
@@ -10,7 +10,7 @@
|
||||
{{ _("With this certification, you can now showcase your updated skills and share your achievement with your colleagues and on LinkedIn. To access your certificate, please click on the link provided below. Make sure you are logged in to the portal.") }}
|
||||
</p>
|
||||
<br>
|
||||
<a href="/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name={{certificate_name}}&format={{template | urlencode }}">{{ _("Certificate Link") }}</a>
|
||||
<a href="/api/method/frappe.utils.print_format.download_pdf?doctype=LMS+Certificate&name={{name}}&format={{template | urlencode }}">{{ _("Certificate Link") }}</a>
|
||||
<br>
|
||||
<p>
|
||||
{{ _("Once again, congratulations on this significant accomplishment.")}}
|
||||
|
||||
124
lms/workspace_sidebar/learning.json
Normal file
124
lms/workspace_sidebar/learning.json
Normal file
@@ -0,0 +1,124 @@
|
||||
{
|
||||
"app": "lms",
|
||||
"creation": "2026-04-06 18:02:13.124002",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace Sidebar",
|
||||
"header_icon": "book",
|
||||
"idx": 0,
|
||||
"items": [
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Courses",
|
||||
"link_to": "LMS Course",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Enrollments",
|
||||
"link_to": "LMS Enrollment",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Course Reviews",
|
||||
"link_to": "LMS Course Review",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Batches",
|
||||
"link_to": "LMS Batch",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Batch Enrollments",
|
||||
"link_to": "LMS Batch Enrollment",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Batch Feedback",
|
||||
"link_to": "LMS Batch Feedback",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Evaluation Requests",
|
||||
"link_to": "LMS Certificate Request",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Evaluations",
|
||||
"link_to": "LMS Certificate Evaluation",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"child": 0,
|
||||
"collapsible": 1,
|
||||
"indent": 0,
|
||||
"keep_closed": 0,
|
||||
"label": "Certificates",
|
||||
"link_to": "LMS Certificate",
|
||||
"link_type": "DocType",
|
||||
"open_in_new_tab": 1,
|
||||
"show_arrow": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2026-04-06 18:04:32.990958",
|
||||
"modified_by": "sayali@frappe.io",
|
||||
"name": "Learning",
|
||||
"owner": "sayali@frappe.io",
|
||||
"standard": 1,
|
||||
"title": "Learning"
|
||||
}
|
||||
Reference in New Issue
Block a user