Compare commits

...

59 Commits

Author SHA1 Message Date
Frappe PR Bot
a8690e41e6 chore(release): Bumped to Version 2.23.0 2025-02-19 05:29:30 +00:00
Jannat Patel
cda42b9ec5 Merge pull request #1325 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-02-19 08:49:19 +05:30
Jannat Patel
21a75fdd6d chore: Bosnian translations 2025-02-18 23:05:44 +05:30
Jannat Patel
a90a1e9855 chore: Persian translations 2025-02-18 23:05:43 +05:30
Jannat Patel
2a046e2e8b chore: German translations 2025-02-18 23:05:40 +05:30
Jannat Patel
bb41656d81 Merge branch 'develop' of https://github.com/frappe/lms into develop 2025-02-18 19:12:23 +05:30
Jannat Patel
a88a107718 fix: batch confirmation email template 2025-02-18 19:12:04 +05:30
Jannat Patel
2d21469f91 Merge pull request #1324 from pateljannat/issues-78
fix: redirect users to the batch page after login
2025-02-18 18:29:51 +05:30
Jannat Patel
960ebe4a79 fix: redirect users to the batch page after login 2025-02-18 18:10:33 +05:30
Jannat Patel
46dba0c394 Merge pull request #1323 from pateljannat/batch-reminders
feat: batch start and live class reminder
2025-02-18 17:34:44 +05:30
Jannat Patel
ba27e8ca95 fix: send live class reminder on the day of the class 2025-02-18 17:26:40 +05:30
Jannat Patel
30574ea0fd feat: batch start and live class reminder 2025-02-18 17:22:52 +05:30
Jannat Patel
c3c985c4a1 Merge pull request #1322 from pateljannat/certification-batches
feat: filter certification batches
2025-02-18 17:05:16 +05:30
Jannat Patel
7b3d2d8812 feat: filter certification batches 2025-02-18 15:57:55 +05:30
Jannat Patel
d573a9f008 Merge pull request #1320 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-02-18 12:41:05 +05:30
Jannat Patel
85a05f56b2 chore: Esperanto translations 2025-02-17 22:33:51 +05:30
Jannat Patel
904adfb905 chore: Bosnian translations 2025-02-17 22:33:50 +05:30
Jannat Patel
b2201c29fd chore: Persian translations 2025-02-17 22:33:48 +05:30
Jannat Patel
fe01f68623 chore: Chinese Simplified translations 2025-02-17 22:33:47 +05:30
Jannat Patel
531c8ebe94 chore: Turkish translations 2025-02-17 22:33:45 +05:30
Jannat Patel
52dfb5a360 chore: Swedish translations 2025-02-17 22:33:44 +05:30
Jannat Patel
7e04e7e461 chore: Russian translations 2025-02-17 22:33:42 +05:30
Jannat Patel
bce47f606d chore: Polish translations 2025-02-17 22:33:41 +05:30
Jannat Patel
4dc1fdfdd8 chore: Hungarian translations 2025-02-17 22:33:40 +05:30
Jannat Patel
9a852b52bc chore: German translations 2025-02-17 22:33:38 +05:30
Jannat Patel
71a57b1fc0 chore: Arabic translations 2025-02-17 22:33:37 +05:30
Jannat Patel
d634598db1 chore: Spanish translations 2025-02-17 22:33:35 +05:30
Jannat Patel
6377d682a4 chore: French translations 2025-02-17 22:33:33 +05:30
Jannat Patel
6e1acfdc24 Merge pull request #1316 from FahidLatheef/fix/quiz-maximum-attempts
fix: fixed bug in which user can submit quiz over the maximum limit allowed
2025-02-17 19:59:57 +05:30
Jannat Patel
30ec1dfd7c Merge pull request #1319 from pateljannat/assignment-grading-comment-field
feat: assignment comments is now text editor
2025-02-17 19:56:22 +05:30
Jannat Patel
3d209024dd fix: height of batch page 2025-02-17 19:45:45 +05:30
Jannat Patel
9ce64a037d fix: increased column width for grading 2025-02-17 19:41:24 +05:30
Jannat Patel
43117bc035 feat:assignment comments is now text editor 2025-02-17 19:28:50 +05:30
Jannat Patel
2af704043e Merge pull request #1318 from pateljannat/batch-email-template
feat: batch specific email templates
2025-02-17 18:36:05 +05:30
Jannat Patel
fa14ffdcba feat: batch specific email templates 2025-02-17 18:17:50 +05:30
Jannat Patel
492b715ea0 Merge pull request #1317 from pateljannat/trial-signup
feat: billing banner for FC trial sites
2025-02-17 16:00:46 +05:30
Jannat Patel
d452e20b8a feat: show trial banner only if fc site 2025-02-17 15:39:15 +05:30
Jannat Patel
6b634c15d9 feat: billing banner for FC trial sites 2025-02-17 15:07:31 +05:30
Jannat Patel
eeaec3369f Merge pull request #1313 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-02-17 11:10:39 +05:30
Jannat Patel
ce1eece90d Merge pull request #1312 from frappe/pot_develop_2025-02-14
chore: update POT file
2025-02-17 11:05:48 +05:30
Jannat Patel
030bff6592 chore: Bosnian translations 2025-02-16 22:32:22 +05:30
Jannat Patel
65de46a59e chore: Swedish translations 2025-02-16 22:32:18 +05:30
Fahid Latheef Alungal
974f67aefe fix: validate if submission exceeds the allowed limit in backend 2025-02-16 19:29:03 +05:30
Fahid Latheef Alungal
e374ae3229 fix: fixed spelling nextQuetion -> nextQuestion 2025-02-16 18:28:48 +05:30
Fahid Latheef Alungal
8b1058e577 fix: fixed issue in which submissions are not reflected gracefully until page reload
ListView throws error if initialized without emptyState which was causing the component to not reload when number of submissions was 0.
2025-02-16 18:27:38 +05:30
Fahid Latheef Alungal
aaa2eea5e6 fix: fixed incomplete router initialization in Quiz.vue which was allowing user to submit quiz multiple times 2025-02-16 18:19:14 +05:30
Fahid Latheef Alungal
54047e3c2c fix: fix spelling typo Maximun Attempts -> Maximum Attempts 2025-02-16 16:10:14 +05:30
Fahid Latheef Alungal
50fe94e47b fix: fix yarn dev not working due to const variable re-assignment
It was causing this error

  ✘ [ERROR] Cannot assign to "isLoggedIn" because it is a constant

    src/router.js:230:2:
      230 │     isLoggedIn = false
          ╵     ~~~~~~~~~~

  The symbol "isLoggedIn" was declared a constant here:

    src/router.js:222:7:
      222 │   const { isLoggedIn } = sessionStore()
          ╵         ^
2025-02-16 16:08:35 +05:30
Jannat Patel
6999f6641a chore: Bosnian translations 2025-02-15 22:29:52 +05:30
frappe-pr-bot
c2b12aa65f chore: update POT file 2025-02-14 16:04:13 +00:00
Jannat Patel
1a731b6908 Merge pull request #1311 from pateljannat/issues-77
fix: students should have access private batch if enrolled
2025-02-14 20:21:54 +05:30
Jannat Patel
837d050628 fix: students should be able to access private batch if they are enrolled 2025-02-14 20:10:32 +05:30
Jannat Patel
8b00bec49c fix: students should be able to access private batch if they are enrolled 2025-02-14 20:04:37 +05:30
Jannat Patel
9ade643af0 Merge pull request #1310 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-02-14 17:06:53 +05:30
Jannat Patel
a29b92a886 chore: Bosnian translations 2025-02-13 22:17:06 +05:30
Jannat Patel
e2c28e211f Merge pull request #1309 from pateljannat/issues-76
fix: misc batch issues
2025-02-13 21:26:17 +05:30
Jannat Patel
65f5b6a0a4 fix: delete unused custom fields from web form 2025-02-13 17:23:57 +05:30
Jannat Patel
75cea1ab78 fix: delete unused custom fields from web form 2025-02-13 17:21:14 +05:30
Jannat Patel
5ab9131629 fix: misc batch issues 2025-02-13 16:57:21 +05:30
52 changed files with 3895 additions and 3513 deletions

View File

@@ -42,6 +42,7 @@
<script> <script>
window.csrf_token = '{{ csrf_token }}' window.csrf_token = '{{ csrf_token }}'
window.setup_complete = '{{ setup_complete }}'
document.getElementById('seo-content').style.display = 'none'; document.getElementById('seo-content').style.display = 'none';
</script> </script>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>

View File

@@ -62,25 +62,34 @@
</div> </div>
</div> </div>
</div> </div>
<SidebarLink <div>
:link="{ <TrialBanner
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse', v-if="
}" userResource.data?.user_type == 'System User' &&
:isCollapsed="sidebarStore.isSidebarCollapsed" userResource.data?.is_fc_site
@click="toggleSidebar()" "
class="m-2" :isSidebarCollapsed="sidebarStore.isSidebarCollapsed"
> />
<template #icon> <SidebarLink
<span class="grid h-5 w-6 flex-shrink-0 place-items-center"> :link="{
<CollapseSidebar label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
class="h-4.5 w-4.5 text-ink-gray-7 duration-300 ease-in-out" }"
:class="{ :isCollapsed="sidebarStore.isSidebarCollapsed"
'[transform:rotateY(180deg)]': sidebarStore.isSidebarCollapsed, @click="toggleSidebar()"
}" class="m-2"
/> >
</span> <template #icon>
</template> <span class="grid h-5 w-6 flex-shrink-0 place-items-center">
</SidebarLink> <CollapseSidebar
class="h-4.5 w-4.5 text-ink-gray-7 duration-300 ease-in-out"
:class="{
'[transform:rotateY(180deg)]': sidebarStore.isSidebarCollapsed,
}"
/>
</span>
</template>
</SidebarLink>
</div>
</div> </div>
<PageModal <PageModal
v-model="showPageModal" v-model="showPageModal"
@@ -101,7 +110,7 @@ import { sessionStore } from '@/stores/session'
import { useSidebar } from '@/stores/sidebar' import { useSidebar } from '@/stores/sidebar'
import { useSettings } from '@/stores/settings' import { useSettings } from '@/stores/settings'
import { ChevronRight, Plus } from 'lucide-vue-next' import { ChevronRight, Plus } from 'lucide-vue-next'
import { createResource, Button } from 'frappe-ui' import { Button, createResource, TrialBanner } from 'frappe-ui'
import PageModal from '@/components/Modals/PageModal.vue' import PageModal from '@/components/Modals/PageModal.vue'
const { user, sidebarSettings } = sessionStore() const { user, sidebarSettings } = sessionStore()

View File

@@ -1,7 +1,7 @@
<template> <template>
<div <div
v-if="assignment.data" v-if="assignment.data"
class="grid grid-cols-[68%,32%] h-full" class="grid grid-cols-[65%,35%] h-full"
:class="{ 'border rounded-lg': !showTitle }" :class="{ 'border rounded-lg': !showTitle }"
> >
<div class="border-r p-5 overflow-y-auto h-[calc(100vh-3.2rem)]"> <div class="border-r p-5 overflow-y-auto h-[calc(100vh-3.2rem)]">
@@ -81,8 +81,8 @@
</template> </template>
</FileUploader> </FileUploader>
<div v-else> <div v-else>
<div class="flex items-center text-ink-gray-7"> <div class="flex text-ink-gray-7">
<div class="border rounded-md p-2 mr-2"> <div class="border self-start rounded-md p-2 mr-2">
<FileText class="h-5 w-5 stroke-1.5" /> <FileText class="h-5 w-5 stroke-1.5" />
</div> </div>
<a <a
@@ -90,7 +90,7 @@
target="_blank" target="_blank"
class="flex flex-col cursor-pointer !no-underline" class="flex flex-col cursor-pointer !no-underline"
> >
<span> <span class="text-sm leading-5">
{{ submissionFile.file_name }} {{ submissionFile.file_name }}
</span> </span>
<span class="text-sm text-ink-gray-5 mt-1"> <span class="text-sm text-ink-gray-5 mt-1">
@@ -155,12 +155,23 @@
type="select" type="select"
:options="submissionStatusOptions" :options="submissionStatusOptions"
/> />
<FormControl <div>
v-if="submissionResource.doc" <div class="text-sm text-ink-gray-5 mb-1">
v-model="submissionResource.doc.comments" {{ __('Comments') }}
:label="__('Comments')" </div>
type="textarea" <TextEditor
/> :content="comments"
@change="
(val) => {
comments = val
isDirty = true
}
"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -184,6 +195,7 @@ import { useRouter } from 'vue-router'
const submissionFile = ref(null) const submissionFile = ref(null)
const answer = ref(null) const answer = ref(null)
const comments = ref(null)
const router = useRouter() const router = useRouter()
const user = inject('$user') const user = inject('$user')
const showTitle = router.currentRoute.value.name == 'AssignmentSubmission' const showTitle = router.currentRoute.value.name == 'AssignmentSubmission'
@@ -281,6 +293,9 @@ watch(submissionResource, () => {
if (submissionResource.doc.answer) { if (submissionResource.doc.answer) {
answer.value = submissionResource.doc.answer answer.value = submissionResource.doc.answer
} }
if (submissionResource.doc.comments) {
comments.value = submissionResource.doc.comments
}
if (submissionResource.isDirty) { if (submissionResource.isDirty) {
isDirty.value = true isDirty.value = true
} else if (showUploader() && !submissionFile.value) { } else if (showUploader() && !submissionFile.value) {
@@ -305,11 +320,14 @@ const submitAssignment = () => {
submissionResource.doc && submissionResource.doc.owner != user.data?.name submissionResource.doc && submissionResource.doc.owner != user.data?.name
? user.data?.name ? user.data?.name
: null : null
submissionResource.setValue.submit( submissionResource.setValue.submit(
{ {
...submissionResource.doc, ...submissionResource.doc,
assignment_attachment: submissionFile.value?.file_url, assignment_attachment: submissionFile.value?.file_url,
evaluator: evaluator, evaluator: evaluator,
comments: comments.value,
answer: answer.value,
}, },
{ {
onSuccess(data) { onSuccess(data) {

View File

@@ -78,7 +78,7 @@
<template #prefix> <template #prefix>
<div v-if="column.key == 'member_name'"> <div v-if="column.key == 'member_name'">
<Avatar <Avatar
class="flex items-center" class="flex"
:image="row['member_image']" :image="row['member_image']"
:label="item" :label="item"
size="sm" size="sm"
@@ -240,6 +240,6 @@ const feedbackColumns = computed(() => {
<style> <style>
.feedback-list > button > div { .feedback-list > button > div {
align-items: start; align-items: start;
padding: 0.25rem 0; padding: 0.15rem 0;
} }
</style> </style>

View File

@@ -2,7 +2,7 @@
<div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72"> <div v-if="batch.data" class="border-2 rounded-md p-5 lg:w-72">
<div <div
v-if="batch.data.seat_count && seats_left > 0" v-if="batch.data.seat_count && seats_left > 0"
class="text-xs bg-green-200 text-green-800 float-right px-2 py-0.5 rounded-md" class="text-xs bg-green-100 text-green-700 float-right px-2 py-0.5 rounded-md"
> >
{{ seats_left }} {{ seats_left }}
<span v-if="seats_left > 1"> <span v-if="seats_left > 1">
@@ -14,7 +14,7 @@
</div> </div>
<div <div
v-else-if="batch.data.seat_count && seats_left <= 0" v-else-if="batch.data.seat_count && seats_left <= 0"
class="text-xs bg-red-200 text-red-900 float-right px-2 py-0.5 rounded-md" class="text-xs bg-red-100 text-red-700 float-right px-2 py-0.5 rounded-md"
> >
{{ __('Sold Out') }} {{ __('Sold Out') }}
</div> </div>

View File

@@ -78,7 +78,7 @@
:options="chartOptions" :options="chartOptions"
:series="chartData" :series="chartData"
type="bar" type="bar"
height="200" :height="chartData[0].data.length * 30 + 100"
/> />
<div <div
class="flex items-center justify-center text-sm text-ink-gray-7 space-x-4" class="flex items-center justify-center text-sm text-ink-gray-7 space-x-4"
@@ -357,7 +357,7 @@ const getChartData = () => {
}) })
Object.keys(student.assessments).forEach((assessment) => { Object.keys(student.assessments).forEach((assessment) => {
if (student.assessments[assessment].result === 'Passed') { if (student.assessments[assessment].result === 'Pass') {
categories[assessment].value += 1 categories[assessment].value += 1
} }
}) })

View File

@@ -67,13 +67,16 @@ const announcement = reactive({
}) })
const announcementResource = createResource({ const announcementResource = createResource({
url: 'lms.lms.api.make_announcement', url: 'frappe.core.doctype.communication.email.make',
makeParams(values) { makeParams(values) {
return { return {
students: props.students, recipients: props.students.join(', '),
cc: announcement.replyTo, cc: announcement.replyTo,
subject: announcement.subject, subject: announcement.subject,
content: announcement.announcement, content: announcement.announcement,
doctype: 'LMS Batch',
name: props.batch,
send_email: 1,
} }
}, },
}) })

View File

@@ -32,25 +32,44 @@
{{ __('Assessment') }} {{ __('Assessment') }}
</span> </span>
<span> <span>
{{ __('Progress') }} {{ __('Percentage/Status') }}
</span> </span>
</div> </div>
<div <router-link
v-for="assessment in Object.keys(student.assessments)" v-for="assessment in Object.keys(student.assessments)"
class="flex items-center text-ink-gray-7 font-medium" class="flex items-center text-ink-gray-7 font-medium"
:to="{
name:
student.assessments[assessment].type == 'LMS Assignment'
? 'AssignmentSubmission'
: '',
params:
student.assessments[assessment].type == 'LMS Assignment'
? {
assignmentID:
student.assessments[assessment].assessment,
submissionName:
student.assessments[assessment].submission,
}
: {},
}"
> >
<span class="flex-1"> <span class="flex-1">
{{ assessment }} {{ assessment }}
</span> </span>
<span v-if="isAssignment(student.assessments[assessment])"> <span v-if="isAssignment(student.assessments[assessment].status)">
<Badge :theme="getStatusTheme(student.assessments[assessment])"> <Badge
{{ student.assessments[assessment] }} :theme="
getStatusTheme(student.assessments[assessment].status)
"
>
{{ student.assessments[assessment].status }}
</Badge> </Badge>
</span> </span>
<span v-else> <span v-else>
{{ student.assessments[assessment] }} {{ student.assessments[assessment].status }}
</span> </span>
</div> </router-link>
</div> </div>
<!-- Courses --> <!-- Courses -->

View File

@@ -0,0 +1,94 @@
<template>
<Dialog
v-model="show"
:options="{
size: 'xl',
title: __('Login to Frappe Cloud'),
actions: [
{
label: __('Verify'),
variant: 'solid',
onClick: (close) => {
verifyCode(close)
},
},
],
}"
>
<template #body-content>
<div>
<p>
{{ __('We have sent the verificaton code to your email id ') }}
<b>{{ props.email }}</b>
</p>
<FormControl
v-model="code"
:label="__('Verification Code')"
class="mb-4"
/>
<p>
{{ __("Didn't receive the code?") }}
<a href="#" @click="resendCode">{{ __('Resend') }}</a>
</p>
</div>
</template>
</Dialog>
</template>
<script setup>
import { call, Dialog } from 'frappe-ui'
import { showToast } from '@/utils'
const show = defineModel()
const code = ref('')
const props = defineProps({
email: {
type: String,
required: true,
},
})
const verifyCode = (close) => {
if (!code.value) {
return
}
call(
'frappe.integrations.frappe_providers.frappecloud_billing.verify_verification_code',
{
verification_code: code.value,
route: window.route,
}
)
.then((data) => {
if (data.message.login_token) {
close()
window.open(
`${frappeCloudBaseEndpoint}/api/method/press.api.developer.saas.login_to_fc?token=${data.message.login_token}`,
'_blank'
)
showToast(
__('Frappe Cloud Login Successful'),
`<p>${__('You will be redirected to Frappe Cloud soon.')}</p><p>${__(
"If you haven't been redirected,"
)} <a href="${frappeCloudBaseEndpoint}/api/method/press.api.developer.saas.login_to_fc?token=${
data.message.login_token
}" target="_blank">${__('Click here to login')}</a></p>`,
'check'
)
} else {
showToast(__('Login failed'), __('Please try again'), 'x')
}
})
.catch((err) => {
showToast(__('Login failed'), __('Please try again'), 'x')
})
}
const resendCode = () => {
call(
'frappe.integrations.frappe_providers.frappecloud_billing.send_verification_code'
).catch((err) => {
showToast(__('Failed to resend code'), __('Please try again'), 'x')
})
}
</script>

View File

@@ -207,7 +207,7 @@
</Button> </Button>
<Button <Button
v-else-if="activeQuestion != questions.length" v-else-if="activeQuestion != questions.length"
@click="nextQuetion()" @click="nextQuestion()"
> >
<span> <span>
{{ __('Next') }} {{ __('Next') }}
@@ -258,14 +258,22 @@
</Button> </Button>
</div> </div>
<div <div
v-if="quiz.data.show_submission_history && attempts?.data" v-if="
quiz.data.show_submission_history &&
attempts?.data &&
attempts.data.length > 0
"
class="mt-10" class="mt-10"
> >
<ListView <ListView
:columns="getSubmissionColumns()" :columns="getSubmissionColumns()"
:rows="attempts?.data" :rows="attempts?.data"
row-key="name" row-key="name"
:options="{ selectable: false, showTooltip: false }" :options="{
selectable: false,
showTooltip: false,
emptyState: { title: __('No Quiz submissions found') },
}"
> >
</ListView> </ListView>
</div> </div>
@@ -282,7 +290,7 @@ import {
FormControl, FormControl,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, watch, reactive, inject, computed } from 'vue' import { ref, watch, reactive, inject, computed } from 'vue'
import { createToast } from '@/utils/' import { createToast, showToast } from '@/utils/'
import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next' import { CheckCircle, XCircle, MinusCircle } from 'lucide-vue-next'
import { timeAgo } from '@/utils' import { timeAgo } from '@/utils'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
@@ -536,7 +544,7 @@ const addToLocalStorage = () => {
localStorage.setItem(quiz.data.title, JSON.stringify(quizData)) localStorage.setItem(quiz.data.title, JSON.stringify(quizData))
} }
const nextQuetion = () => { const nextQuestion = () => {
if (!quiz.data.show_answers && questionDetails.data?.type != 'Open Ended') { if (!quiz.data.show_answers && questionDetails.data?.type != 'Open Ended') {
checkAnswer() checkAnswer()
} else { } else {
@@ -574,6 +582,16 @@ const createSubmission = () => {
if (quiz.data && quiz.data.max_attempts) attempts.reload() if (quiz.data && quiz.data.max_attempts) attempts.reload()
if (quiz.data.duration) clearInterval(timerInterval) if (quiz.data.duration) clearInterval(timerInterval)
}, },
onError(err) {
const errorTitle = err?.message || ''
if (errorTitle.includes('MaximumAttemptsExceededError')) {
const errorMessage = err.messages?.[0] || err
showToast(__('Error'), __(errorMessage), 'x')
setTimeout(() => {
window.location.reload()
}, 3000)
}
},
} }
) )
} }

View File

@@ -59,13 +59,22 @@
v-if="userResource.data?.is_moderator" v-if="userResource.data?.is_moderator"
v-model="showSettingsModal" v-model="showSettingsModal"
/> />
<FCVerfiyCodeModal v-if="showFCLoginDialog" :email="verificationEmail" />
</template> </template>
<script setup> <script setup>
import LMSLogo from '@/components/Icons/LMSLogo.vue' import LMSLogo from '@/components/Icons/LMSLogo.vue'
import { sessionStore } from '@/stores/session' import { sessionStore } from '@/stores/session'
import { Dropdown } from 'frappe-ui' import { call, Dropdown } from 'frappe-ui'
import Apps from '@/components/Apps.vue' import Apps from '@/components/Apps.vue'
import { useRouter } from 'vue-router'
import { convertToTitleCase, showToast } from '@/utils'
import { usersStore } from '@/stores/user'
import { useSettings } from '@/stores/settings'
import { markRaw, watch, ref, onMounted, computed } from 'vue'
import SettingsModal from '@/components/Modals/Settings.vue'
import { createDialog } from '@/utils/dialogs'
import FCVerfiyCodeModal from './Modals/FCVerfiyCodeModal.vue'
import { import {
ChevronDown, ChevronDown,
LogIn, LogIn,
@@ -74,13 +83,8 @@ import {
User, User,
Settings, Settings,
Sun, Sun,
LogInIcon,
} from 'lucide-vue-next' } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import { convertToTitleCase } from '../utils'
import { usersStore } from '@/stores/user'
import { useSettings } from '@/stores/settings'
import { markRaw, watch, ref, onMounted, computed } from 'vue'
import SettingsModal from '@/components/Modals/Settings.vue'
const router = useRouter() const router = useRouter()
const { logout, branding } = sessionStore() const { logout, branding } = sessionStore()
@@ -89,6 +93,11 @@ const settingsStore = useSettings()
let { isLoggedIn } = sessionStore() let { isLoggedIn } = sessionStore()
const showSettingsModal = ref(false) const showSettingsModal = ref(false)
const theme = ref('light') const theme = ref('light')
const $dialog = createDialog
const frappeCloudBaseEndpoint = 'https://frappecloud.com'
const showFCLoginDialog = ref(false)
const verificationEmail = ref(null)
const props = defineProps({ const props = defineProps({
isCollapsed: { isCollapsed: {
@@ -130,6 +139,13 @@ const userDropdownOptions = computed(() => {
return isLoggedIn return isLoggedIn
}, },
}, },
{
icon: theme.value === 'light' ? Moon : Sun,
label: 'Toggle Theme',
onClick: () => {
toggleTheme()
},
},
{ {
component: markRaw(Apps), component: markRaw(Apps),
condition: () => { condition: () => {
@@ -139,13 +155,6 @@ const userDropdownOptions = computed(() => {
else return false else return false
}, },
}, },
{
icon: theme.value === 'light' ? Moon : Sun,
label: 'Toggle Theme',
onClick: () => {
toggleTheme()
},
},
{ {
icon: Settings, icon: Settings,
label: 'Settings', label: 'Settings',
@@ -156,6 +165,19 @@ const userDropdownOptions = computed(() => {
return userResource.data?.is_moderator return userResource.data?.is_moderator
}, },
}, },
{
icon: LogInIcon,
label: 'Login to Frappe Cloud',
onClick: () => {
initiateRequestForLoginToFrappeCloud()
},
condition: () => {
return (
userResource.data?.user_type == 'System User' &&
userResource.data?.is_fc_site
)
},
},
{ {
icon: LogOut, icon: LogOut,
label: 'Log out', label: 'Log out',
@@ -180,4 +202,48 @@ const userDropdownOptions = computed(() => {
}, },
] ]
}) })
const initiateRequestForLoginToFrappeCloud = () => {
$dialog({
title: __('Login to Frappe Cloud?'),
message: __(
'Are you sure you want to login to your Frappe Cloud dashboard?'
),
actions: [
{
label: __('Confirm'),
variant: 'solid',
onClick(close) {
requestLoginToFC()
close()
},
},
],
})
}
const requestLoginToFC = () => {
call(
'frappe.integrations.frappe_providers.frappecloud_billing.send_verification_code'
)
.then((data) => {
if (data.message.is_user_logged_in) {
window.open(
`${frappeCloudBaseEndpoint}${data.message.redirect_to}`,
'_blank'
)
return
} else {
showFCLoginDialog.value = true
verificationEmail.value = data.message.email
}
})
.catch((err) => {
showToast(
__('Failed to login to Frappe Cloud'),
__('Please try again'),
'x'
)
})
}
</script> </script>

View File

@@ -6,7 +6,7 @@
<Breadcrumbs class="h-7" :items="breadcrumbs" /> <Breadcrumbs class="h-7" :items="breadcrumbs" />
<div class="flex items-center space-x-2"> <div class="flex items-center space-x-2">
<Button <Button
v-if="user.data?.is_moderator" v-if="user.data?.is_moderator && batch.data?.certification"
@click="openCertificateDialog = true" @click="openCertificateDialog = true"
> >
{{ __('Generate Certificates') }} {{ __('Generate Certificates') }}
@@ -21,7 +21,10 @@
</Button> </Button>
</div> </div>
</header> </header>
<div v-if="batch.data" class="grid grid-cols-[75%,25%]"> <div
v-if="batch.data"
class="grid grid-cols-[75%,25%] h-[calc(100vh-3.2rem)]"
>
<div class="border-r"> <div class="border-r">
<Tabs <Tabs
v-model="tabIndex" v-model="tabIndex"
@@ -310,7 +313,7 @@ const tabs = computed(() => {
}) })
const redirectToLogin = () => { const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/batches` window.location.href = `/login?redirect-to=/lms/batches/${props.batchName}`
} }
const openAnnouncementModal = () => { const openAnnouncementModal = () => {

View File

@@ -127,6 +127,11 @@ const batch = createResource({
batch: props.batchName, batch: props.batchName,
}, },
auto: true, auto: true,
onSuccess: (data) => {
if (!data) {
router.push({ name: 'Batches' })
}
},
}) })
const courses = createResource({ const courses = createResource({

View File

@@ -13,15 +13,14 @@
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
{{ __('Details') }} {{ __('Details') }}
</div> </div>
<div class="grid grid-cols-2 gap-10 mb-4 space-y-2"> <div class="space-y-4 mb-4">
<div> <FormControl
<FormControl v-model="batch.title"
v-model="batch.title" :label="__('Title')"
:label="__('Title')" :required="true"
:required="true" class="w-full"
/> />
</div> <div class="flex items-center space-x-5">
<div class="flex flex-col space-y-2">
<FormControl <FormControl
v-model="batch.published" v-model="batch.published"
type="checkbox" type="checkbox"
@@ -32,6 +31,11 @@
type="checkbox" type="checkbox"
:label="__('Allow self enrollment')" :label="__('Allow self enrollment')"
/> />
<FormControl
v-model="batch.certification"
type="checkbox"
:label="__('Certification')"
/>
</div> </div>
</div> </div>
</div> </div>
@@ -90,30 +94,8 @@
:required="true" :required="true"
:filters="{ ignore_user_type: 1 }" :filters="{ ignore_user_type: 1 }"
/> />
<div class="mb-4">
<FormControl <div class="my-10">
v-model="batch.description"
:label="__('Description')"
type="textarea"
class="my-4"
:placeholder="__('Short description of the batch')"
:required="true"
/>
<div>
<label class="block text-sm text-ink-gray-5 mb-1">
{{ __('Batch Details') }}
<span class="text-ink-red-3">*</span>
</label>
<TextEditor
:content="batch.batch_details"
@change="(val) => (batch.batch_details = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
/>
</div>
</div>
<div class="mb-4">
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
{{ __('Date and Time') }} {{ __('Date and Time') }}
</div> </div>
@@ -133,6 +115,14 @@
class="mb-4" class="mb-4"
:required="true" :required="true"
/> />
<FormControl
v-model="batch.timezone"
:label="__('Timezone')"
type="text"
:placeholder="__('Example: IST (+5:30)')"
class="mb-4"
:required="true"
/>
</div> </div>
<div> <div>
<FormControl <FormControl
@@ -149,18 +139,11 @@
class="mb-4" class="mb-4"
:required="true" :required="true"
/> />
<FormControl
v-model="batch.timezone"
:label="__('Timezone')"
type="text"
:placeholder="__('Example: IST (+5:30)')"
class="mb-4"
:required="true"
/>
</div> </div>
</div> </div>
</div> </div>
<div class="mb-4">
<div class="mb-10">
<div class="text-lg font-semibold mb-4"> <div class="text-lg font-semibold mb-4">
{{ __('Settings') }} {{ __('Settings') }}
</div> </div>
@@ -179,6 +162,11 @@
type="date" type="date"
class="mb-4" class="mb-4"
/> />
<Link
doctype="Email Template"
:label="__('Email Template')"
v-model="batch.confirmation_email_template"
/>
</div> </div>
<div> <div>
<FormControl <FormControl
@@ -230,6 +218,33 @@
/> />
</div> </div>
</div> </div>
<div class="my-10">
<div class="text-lg font-semibold mb-4">
{{ __('Description') }}
</div>
<FormControl
v-model="batch.description"
:label="__('Short Description')"
type="textarea"
class="my-4"
:placeholder="__('Short description of the batch')"
:required="true"
/>
<div>
<label class="block text-sm text-ink-gray-5 mb-1">
{{ __('Batch Details') }}
<span class="text-ink-red-3">*</span>
</label>
<TextEditor
:content="batch.batch_details"
@change="(val) => (batch.batch_details = val)"
:editable="true"
:fixedMenu="true"
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem] mb-4"
/>
</div>
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -278,10 +293,12 @@ const batch = reactive({
end_time: '', end_time: '',
timezone: '', timezone: '',
evaluation_end_date: '', evaluation_end_date: '',
confirmation_email_template: '',
seat_count: '', seat_count: '',
medium: '', medium: '',
category: '', category: '',
allow_self_enrollment: false, allow_self_enrollment: false,
certification: false,
image: null, image: null,
paid_batch: false, paid_batch: false,
currency: '', currency: '',
@@ -351,7 +368,12 @@ const batchDetail = createResource({
batch[key] = `${hours}:${minutes}` batch[key] = `${hours}:${minutes}`
} else if (Object.hasOwn(batch, key)) batch[key] = data[key] } else if (Object.hasOwn(batch, key)) batch[key] = data[key]
}) })
let checkboxes = ['published', 'paid_batch', 'allow_self_enrollment'] let checkboxes = [
'published',
'paid_batch',
'allow_self_enrollment',
'certification',
]
for (let idx in checkboxes) { for (let idx in checkboxes) {
let key = checkboxes[idx] let key = checkboxes[idx]
batch[key] = batch[key] ? true : false batch[key] = batch[key] ? true : false

View File

@@ -26,13 +26,19 @@
{{ __('All Batches') }} {{ __('All Batches') }}
</div> </div>
<div <div
class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-2" class="flex flex-col space-y-2 lg:space-y-0 lg:flex-row lg:items-center lg:space-x-4"
> >
<TabButtons <TabButtons
v-if="user.data" v-if="user.data"
:buttons="batchTabs" :buttons="batchTabs"
v-model="currentTab" v-model="currentTab"
/> />
<FormControl
v-model="certification"
:label="__('Certification')"
type="checkbox"
@change="updateBatches()"
/>
<div class="grid grid-cols-2 gap-2"> <div class="grid grid-cols-2 gap-2">
<FormControl <FormControl
v-model="title" v-model="title"
@@ -111,6 +117,7 @@ const pageLength = ref(20)
const categories = ref([]) const categories = ref([])
const currentCategory = ref(null) const currentCategory = ref(null)
const title = ref('') const title = ref('')
const certification = ref(false)
const filters = ref({}) const filters = ref({})
const currentTab = ref(user.data?.is_student ? 'All' : 'Upcoming') const currentTab = ref(user.data?.is_student ? 'All' : 'Upcoming')
const orderBy = ref('start_date') const orderBy = ref('start_date')
@@ -130,6 +137,7 @@ const setFiltersFromQuery = () => {
let queries = new URLSearchParams(location.search) let queries = new URLSearchParams(location.search)
title.value = queries.get('title') || '' title.value = queries.get('title') || ''
currentCategory.value = queries.get('category') || null currentCategory.value = queries.get('category') || null
certification.value = queries.get('certification') || false
} }
const batches = createListResource({ const batches = createListResource({
@@ -161,6 +169,7 @@ const updateBatches = () => {
const updateFilters = () => { const updateFilters = () => {
updateCategoryFilter() updateCategoryFilter()
updateTitleFilter() updateTitleFilter()
updateCertificationFilter()
updateTabFilter() updateTabFilter()
updateStudentFilter() updateStudentFilter()
setQueryParams() setQueryParams()
@@ -182,6 +191,14 @@ const updateTitleFilter = () => {
} }
} }
const updateCertificationFilter = () => {
if (certification.value) {
filters.value['certification'] = 1
} else {
delete filters.value['certification']
}
}
const updateTabFilter = () => { const updateTabFilter = () => {
orderBy.value = 'start_date' orderBy.value = 'start_date'
if (!user.data) { if (!user.data) {
@@ -222,6 +239,7 @@ const setQueryParams = () => {
let filterKeys = { let filterKeys = {
title: title.value, title: title.value,
category: currentCategory.value, category: currentCategory.value,
certification: certification.value,
} }
Object.keys(filterKeys).forEach((key) => { Object.keys(filterKeys).forEach((key) => {

View File

@@ -55,7 +55,7 @@
<FormControl <FormControl
type="number" type="number"
v-model="quiz.max_attempts" v-model="quiz.max_attempts"
:label="__('Maximun Attempts')" :label="__('Maximum Attempts')"
/> />
<FormControl <FormControl
type="number" type="number"

View File

@@ -219,7 +219,7 @@ let router = createRouter({
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const { userResource } = usersStore() const { userResource } = usersStore()
const { isLoggedIn } = sessionStore() let { isLoggedIn } = sessionStore()
const { allowGuestAccess } = useSettings() const { allowGuestAccess } = useSettings()
try { try {

View File

@@ -4,6 +4,7 @@ import { createApp, h } from 'vue'
import { usersStore } from '../stores/user' import { usersStore } from '../stores/user'
import translationPlugin from '../translation' import translationPlugin from '../translation'
import { CircleHelp } from 'lucide-vue-next' import { CircleHelp } from 'lucide-vue-next'
import router from '@/router'
export class Quiz { export class Quiz {
constructor({ data, api, readOnly }) { constructor({ data, api, readOnly }) {
@@ -46,6 +47,7 @@ export class Quiz {
quiz: quiz, quiz: quiz,
}) })
app.use(translationPlugin) app.use(translationPlugin)
app.use(router)
const { userResource } = usersStore() const { userResource } = usersStore()
app.provide('$user', userResource) app.provide('$user', userResource)
app.mount(this.wrapper) app.mount(this.wrapper)

View File

@@ -15,7 +15,7 @@ export default defineConfig({
}), }),
], ],
server: { server: {
allowedHosts: ['fs'], allowedHosts: ['fs', 'bs'],
}, },
resolve: { resolve: {
alias: { alias: {

View File

@@ -1 +1 @@
__version__ = "2.22.0" __version__ = "2.23.0"

View File

@@ -951,62 +951,6 @@
"unique": 0, "unique": 0,
"width": null "width": null
}, },
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Web Form",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "payments_tab",
"fieldtype": "Tab Break",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "custom_css",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Payments",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2024-09-19 08:35:17.329032",
"module": null,
"name": "Web Form-payments_tab",
"no_copy": 0,
"non_negative": 0,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{ {
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
@@ -1119,230 +1063,6 @@
"unique": 0, "unique": 0,
"width": null "width": null
}, },
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": "accept_payment",
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Web Form",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "payment_gateway",
"fieldtype": "Link",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "accept_payment",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Payment Gateway",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2024-09-19 08:35:17.408659",
"module": null,
"name": "Web Form-payment_gateway",
"no_copy": 0,
"non_negative": 0,
"options": "Payment Gateway",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": "Buy Now",
"depends_on": "accept_payment",
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Web Form",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "payment_button_label",
"fieldtype": "Data",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "payment_gateway",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Button Label",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2024-09-19 08:35:17.439246",
"module": null,
"name": "Web Form-payment_button_label",
"no_copy": 0,
"non_negative": 0,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": null,
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Web Form",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "payments_cb",
"fieldtype": "Column Break",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "payment_button_help",
"is_system_generated": 1,
"is_virtual": 0,
"label": null,
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2024-09-19 08:35:17.491696",
"module": null,
"name": "Web Form-payments_cb",
"no_copy": 0,
"non_negative": 0,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": "eval:doc.accept_payment && !doc.amount_based_on_field",
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Web Form",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "amount",
"fieldtype": "Currency",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "amount_field",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Amount",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2024-09-19 08:35:17.569591",
"module": null,
"name": "Web Form-amount",
"no_copy": 0,
"non_negative": 0,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{ {
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
@@ -1399,174 +1119,6 @@
"unique": 0, "unique": 0,
"width": null "width": null
}, },
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": "accept_payment",
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Web Form",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "payment_button_help",
"fieldtype": "Text",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "payment_button_label",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Button Help",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2024-09-19 08:35:17.466744",
"module": null,
"name": "Web Form-payment_button_help",
"no_copy": 0,
"non_negative": 0,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": "0",
"depends_on": "accept_payment",
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Web Form",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "amount_based_on_field",
"fieldtype": "Check",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "payments_cb",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Amount Based On Field",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2024-09-19 08:35:17.517344",
"module": null,
"name": "Web Form-amount_based_on_field",
"no_copy": 0,
"non_negative": 0,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": "eval:doc.accept_payment && doc.amount_based_on_field",
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Web Form",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "amount_field",
"fieldtype": "Select",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "amount_based_on_field",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Amount Field",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2024-09-19 08:35:17.543136",
"module": null,
"name": "Web Form-amount_field",
"no_copy": 0,
"non_negative": 0,
"options": null,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{ {
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,
@@ -1679,62 +1231,6 @@
"unique": 0, "unique": 0,
"width": null "width": null
}, },
{
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"collapsible_depends_on": null,
"columns": 0,
"default": null,
"depends_on": "accept_payment",
"description": null,
"docstatus": 0,
"doctype": "Custom Field",
"dt": "Web Form",
"fetch_from": null,
"fetch_if_empty": 0,
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 0,
"hide_border": 0,
"hide_days": 0,
"hide_seconds": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_preview": 0,
"in_standard_filter": 0,
"insert_after": "amount",
"is_system_generated": 1,
"is_virtual": 0,
"label": "Currency",
"length": 0,
"link_filters": null,
"mandatory_depends_on": null,
"modified": "2024-09-19 08:35:17.595419",
"module": null,
"name": "Web Form-currency",
"no_copy": 0,
"non_negative": 0,
"options": "Currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": null,
"read_only": 0,
"read_only_depends_on": null,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"show_dashboard": 0,
"sort_options": 0,
"translatable": 0,
"unique": 0,
"width": null
},
{ {
"allow_in_quick_entry": 0, "allow_in_quick_entry": 0,
"allow_on_submit": 0, "allow_on_submit": 0,

View File

@@ -116,6 +116,8 @@ scheduler_events = {
"daily": [ "daily": [
"lms.job.doctype.job_opportunity.job_opportunity.update_job_openings", "lms.job.doctype.job_opportunity.job_opportunity.update_job_openings",
"lms.lms.doctype.lms_payment.lms_payment.send_payment_reminder", "lms.lms.doctype.lms_payment.lms_payment.send_payment_reminder",
"lms.lms.doctype.lms_batch.lms_batch.send_batch_start_reminder",
"lms.lms.doctype.lms_live_class.lms_live_class.send_live_class_reminder",
], ],
} }

View File

@@ -7,15 +7,10 @@ import zipfile
import os import os
import re import re
import shutil import shutil
import requests
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from frappe.translate import get_all_translations from frappe.translate import get_all_translations
from frappe import _ from frappe import _
from frappe.query_builder import DocType
from frappe.query_builder.functions import Count
from frappe.utils import ( from frappe.utils import (
time_diff,
now_datetime,
get_datetime, get_datetime,
cint, cint,
flt, flt,
@@ -24,10 +19,10 @@ from frappe.utils import (
format_date, format_date,
date_diff, date_diff,
) )
from typing import Optional
from lms.lms.utils import get_average_rating, get_lesson_count from lms.lms.utils import get_average_rating, get_lesson_count
from xml.dom.minidom import parseString from xml.dom.minidom import parseString
from lms.lms.doctype.course_lesson.course_lesson import save_progress from lms.lms.doctype.course_lesson.course_lesson import save_progress
from frappe.integrations.frappe_providers.frappecloud_billing import is_fc_site
@frappe.whitelist() @frappe.whitelist()
@@ -180,6 +175,7 @@ def get_user_info():
user.is_moderator = "Moderator" in user.roles user.is_moderator = "Moderator" in user.roles
user.is_evaluator = "Batch Evaluator" in user.roles user.is_evaluator = "Batch Evaluator" in user.roles
user.is_student = "LMS Student" in user.roles user.is_student = "LMS Student" in user.roles
user.is_fc_site = is_fc_site()
return user return user
@@ -845,7 +841,7 @@ def update_course_statistics():
@frappe.whitelist() @frappe.whitelist()
def get_announcements(batch): def get_announcements(batch):
return frappe.get_all( communications = frappe.get_all(
"Communication", "Communication",
filters={ filters={
"reference_doctype": "LMS Batch", "reference_doctype": "LMS Batch",
@@ -863,6 +859,13 @@ def get_announcements(batch):
order_by="communication_date desc", order_by="communication_date desc",
) )
for communication in communications:
communication.image = frappe.get_cached_value(
"User", communication.sender, "user_image"
)
return communications
@frappe.whitelist() @frappe.whitelist()
def delete_course(course): def delete_course(course):
@@ -1225,16 +1228,3 @@ def get_notifications(filters):
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def is_guest_allowed(): def is_guest_allowed():
return frappe.get_cached_value("LMS Settings", None, "allow_guest_access") return frappe.get_cached_value("LMS Settings", None, "allow_guest_access")
@frappe.whitelist()
def make_announcement(students, cc, subject, content):
for student in students:
frappe.sendmail(
recipients=student,
cc=cc,
subject=subject,
message=content,
header=[subject, "green"],
retry=3,
)

View File

@@ -13,11 +13,12 @@
"column_break_3", "column_break_3",
"member", "member",
"member_name", "member_name",
"evaluator",
"section_break_dlzh", "section_break_dlzh",
"assignment_attachment", "assignment_attachment",
"answer", "answer",
"section_break_ydgh",
"column_break_oqqy", "column_break_oqqy",
"evaluator",
"status", "status",
"comments", "comments",
"section_break_rqal", "section_break_rqal",
@@ -80,7 +81,7 @@
}, },
{ {
"fieldname": "comments", "fieldname": "comments",
"fieldtype": "Small Text", "fieldtype": "Text Editor",
"label": "Comments" "label": "Comments"
}, },
{ {
@@ -139,12 +140,16 @@
{ {
"fieldname": "column_break_oqqy", "fieldname": "column_break_oqqy",
"fieldtype": "Column Break" "fieldtype": "Column Break"
},
{
"fieldname": "section_break_ydgh",
"fieldtype": "Section Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2024-12-24 21:22:35.212732", "modified": "2025-02-17 18:40:53.374932",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Assignment Submission", "name": "LMS Assignment Submission",

View File

@@ -8,25 +8,31 @@
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"published", "section_break_earo",
"title", "title",
"start_date", "start_date",
"end_date", "end_date",
"column_break_4", "column_break_4",
"allow_self_enrollment",
"start_time", "start_time",
"end_time", "end_time",
"timezone", "timezone",
"section_break_rgfj", "section_break_cssv",
"medium", "published",
"category", "column_break_wfkz",
"column_break_flwy", "allow_self_enrollment",
"seat_count", "column_break_vnrp",
"evaluation_end_date", "certification",
"section_break_6", "section_break_6",
"description", "description",
"column_break_hlqw", "column_break_hlqw",
"instructors", "instructors",
"section_break_rgfj",
"medium",
"category",
"confirmation_email_template",
"column_break_flwy",
"seat_count",
"evaluation_end_date",
"meta_image", "meta_image",
"section_break_khcn", "section_break_khcn",
"batch_details", "batch_details",
@@ -206,6 +212,7 @@
"default": "0", "default": "0",
"fieldname": "published", "fieldname": "published",
"fieldtype": "Check", "fieldtype": "Check",
"in_standard_filter": 1,
"label": "Published" "label": "Published"
}, },
{ {
@@ -318,6 +325,35 @@
{ {
"fieldname": "section_break_khcn", "fieldname": "section_break_khcn",
"fieldtype": "Section Break" "fieldtype": "Section Break"
},
{
"fieldname": "confirmation_email_template",
"fieldtype": "Link",
"label": "Confirmation Email Template",
"options": "Email Template"
},
{
"fieldname": "column_break_wfkz",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_vnrp",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "certification",
"fieldtype": "Check",
"in_standard_filter": 1,
"label": "Certification"
},
{
"fieldname": "section_break_earo",
"fieldtype": "Section Break"
},
{
"fieldname": "section_break_cssv",
"fieldtype": "Section Break"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
@@ -335,7 +371,7 @@
"link_fieldname": "batch_name" "link_fieldname": "batch_name"
} }
], ],
"modified": "2025-02-12 11:59:35.312487", "modified": "2025-02-18 15:43:18.512504",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Batch", "name": "LMS Batch",

View File

@@ -8,7 +8,7 @@ import json
from frappe import _ from frappe import _
from datetime import timedelta from datetime import timedelta
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, format_datetime, get_time from frappe.utils import cint, format_datetime, get_time, add_days, nowdate
from lms.lms.utils import ( from lms.lms.utils import (
get_lessons, get_lessons,
get_lesson_index, get_lesson_index,
@@ -405,3 +405,40 @@ def is_milestone_complete(idx, batch):
return False return False
return True return True
def send_batch_start_reminder():
batches = frappe.get_all(
"LMS Batch",
{"start_date": add_days(nowdate(), 1), "published": 1},
["name", "title", "start_date", "start_time", "medium"],
)
for batch in batches:
students = frappe.get_all(
"LMS Batch Enrollment", {"batch": batch}, ["member", "member_name"]
)
for student in students:
send_mail(batch, student)
def send_mail(batch, student):
subject = _("Batch Start Reminder")
template = "batch_start_reminder"
args = {
"student_name": student.member_name,
"title": batch.title,
"start_date": batch.start_date,
"start_time": batch.start_time,
"medium": batch.medium,
"name": batch.name,
}
frappe.sendmail(
recipients=student.member,
subject=subject,
template=template,
args=args,
header=[_(f"Batch Start Reminder: {batch.title}"), "orange"],
)

View File

@@ -76,17 +76,26 @@ def send_confirmation_email(doc):
def send_mail(doc): def send_mail(doc):
subject = _("Enrollment Confirmation for the Next Training Batch")
template = "batch_confirmation"
custom_template = frappe.db.get_single_value(
"LMS Settings", "batch_confirmation_template"
)
batch = frappe.db.get_value( batch = frappe.db.get_value(
"LMS Batch", "LMS Batch",
doc.batch, doc.batch,
["name", "title", "start_date", "start_time", "medium"], [
"name",
"title",
"start_date",
"start_time",
"medium",
"confirmation_email_template",
],
as_dict=1, as_dict=1,
) )
subject = _("Enrollment Confirmation for {0}").format(batch.title)
template = "batch_confirmation"
custom_template = batch.confirmation_email_template or frappe.db.get_single_value(
"LMS Settings", "batch_confirmation_template"
)
args = { args = {
"title": batch.title, "title": batch.title,
"student_name": doc.member_name, "student_name": doc.member_name,
@@ -107,6 +116,6 @@ def send_mail(doc):
template=template if not custom_template else None, template=template if not custom_template else None,
content=content if custom_template else None, content=content if custom_template else None,
args=args, args=args,
header=[subject, "green"], header=[_(batch.title), "green"],
retry=3, retry=3,
) )

View File

@@ -2,9 +2,10 @@
# For license information, please see license.txt # For license information, please see license.txt
import frappe import frappe
from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from datetime import timedelta from datetime import timedelta
from frappe.utils import cint, get_datetime from frappe.utils import cint, get_datetime, format_date, nowdate, format_time
class LMSLiveClass(Document): class LMSLiveClass(Document):
@@ -56,8 +57,48 @@ class LMSLiveClass(Document):
{ {
"sync_with_google_calendar": 1, "sync_with_google_calendar": 1,
"google_calendar": calendar, "google_calendar": calendar,
"description": f"A Live Class has been scheduled on {frappe.utils.format_date(self.date, 'medium')} at { frappe.utils.format_time(self.time, 'hh:mm a')}. Click on this link to join. {self.join_url}. {self.description}", "description": f"A Live Class has been scheduled on {format_date(self.date, 'medium')} at {format_time(self.time, 'hh:mm a')}. Click on this link to join. {self.join_url}. {self.description}",
} }
) )
event.save() event.save()
def send_live_class_reminder():
classes = frappe.get_all(
"LMS Live Class",
{
"date": nowdate(),
},
["name", "batch_name", "title", "date", "time"],
)
for live_class in classes:
students = frappe.get_all(
"LMS Batch Enrollment",
{"batch": live_class.batch_name},
["member", "member_name"],
)
for student in students:
send_mail(live_class, student)
def send_mail(live_class, student):
subject = f"Your class on {live_class.title} is tomorrow"
template = "live_class_reminder"
args = {
"student_name": student.member_name,
"title": live_class.title,
"date": live_class.date,
"time": live_class.time,
"batch_name": live_class.batch_name,
}
frappe.sendmail(
recipients=student.member,
subject=subject,
template=template,
args=args,
header=[_(f"Class Reminder: {live_class.title}"), "orange"],
)

View File

@@ -139,8 +139,17 @@
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [
"modified": "2025-02-11 14:48:27.801895", {
"link_doctype": "LMS Batch Enrollment",
"link_fieldname": "payment"
},
{
"link_doctype": "LMS Enrollment",
"link_fieldname": "payment"
}
],
"modified": "2025-02-18 15:54:25.383353",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Payment", "name": "LMS Payment",

View File

@@ -10,12 +10,29 @@ from frappe.desk.doctype.notification_log.notification_log import make_notificat
class LMSQuizSubmission(Document): class LMSQuizSubmission(Document):
def validate(self): def validate(self):
self.validate_if_max_attempts_exceeded()
self.validate_marks() self.validate_marks()
self.set_percentage() self.set_percentage()
def on_update(self): def on_update(self):
self.notify_member() self.notify_member()
def validate_if_max_attempts_exceeded(self):
max_attempts = frappe.db.get_value("LMS Quiz", self.quiz, ["max_attempts"])
if max_attempts == 0:
return
current_user_submission_count = frappe.db.count(
self.doctype, filters={"quiz": self.quiz, "member": frappe.session.user}
)
if current_user_submission_count >= max_attempts:
frappe.throw(
_("You have exceeded the maximum number of attempts ({0}) for this quiz").format(
max_attempts
),
MaximumAttemptsExceededError,
)
def validate_marks(self): def validate_marks(self):
self.score = 0 self.score = 0
for row in self.result: for row in self.result:
@@ -52,3 +69,7 @@ class LMSQuizSubmission(Document):
) )
make_notification_logs(notification, [self.member]) make_notification_logs(notification, [self.member])
class MaximumAttemptsExceededError(frappe.DuplicateEntryError):
pass

View File

@@ -1194,6 +1194,16 @@ def get_neighbour_lesson(course, chapter, lesson):
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def get_batch_details(batch): def get_batch_details(batch):
batch_students = frappe.get_all(
"LMS Batch Enrollment", {"batch": batch}, pluck="member"
)
if (
not frappe.db.get_value("LMS Batch", batch, "published")
and has_student_role()
and frappe.session.user not in batch_students
):
return
batch_details = frappe.db.get_value( batch_details = frappe.db.get_value(
"LMS Batch", "LMS Batch",
batch, batch,
@@ -1215,6 +1225,7 @@ def get_batch_details(batch):
"paid_batch", "paid_batch",
"evaluation_end_date", "evaluation_end_date",
"allow_self_enrollment", "allow_self_enrollment",
"certification",
"timezone", "timezone",
"category", "category",
], ],
@@ -1226,9 +1237,7 @@ def get_batch_details(batch):
batch_details.courses = frappe.get_all( batch_details.courses = frappe.get_all(
"Batch Course", filters={"parent": batch}, fields=["course", "title", "evaluator"] "Batch Course", filters={"parent": batch}, fields=["course", "title", "evaluator"]
) )
batch_details.students = frappe.get_all( batch_details.students = batch_students
"LMS Batch Enrollment", {"batch": batch}, pluck="member"
)
if batch_details.paid_batch and batch_details.start_date >= getdate(): if batch_details.paid_batch and batch_details.start_date >= getdate():
batch_details.amount, batch_details.currency = check_multicurrency( batch_details.amount, batch_details.currency = check_multicurrency(
@@ -1450,7 +1459,7 @@ def get_batch_students(batch):
) )
detail.assessments[title] = assessment_info detail.assessments[title] = assessment_info
if assessment_info.result == "Passed": if assessment_info.result == "Pass":
assessments_completed += 1 assessments_completed += 1
detail.courses_completed = courses_completed detail.courses_completed = courses_completed
@@ -1493,20 +1502,26 @@ def has_submitted_assessment(assessment, assessment_type, member=None):
attempt = frappe.db.exists(doctype, filters) attempt = frappe.db.exists(doctype, filters)
if attempt: if attempt:
attempt_details = frappe.db.get_value(doctype, filters, fields) fields.append("name")
attempt_details = frappe.db.get_value(doctype, filters, fields, as_dict=1)
if assessment_type == "LMS Quiz": if assessment_type == "LMS Quiz":
result = "Failed" result = "Failed"
passing_percentage = frappe.db.get_value( passing_percentage = frappe.db.get_value(
"LMS Quiz", assessment, "passing_percentage" "LMS Quiz", assessment, "passing_percentage"
) )
if attempt_details >= passing_percentage: if attempt_details.percentage >= passing_percentage:
result = "Passed" result = "Pass"
else: else:
result = attempt_details result = attempt_details.status
return frappe._dict( return frappe._dict(
{ {
"status": attempt_details, "status": attempt_details.percentage
if assessment_type == "LMS Quiz"
else attempt_details.status,
"result": result, "result": result,
"assessment": assessment,
"type": assessment_type,
"submission": attempt_details.name,
} }
) )
else: else:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -100,3 +100,4 @@ lms.patches.v2_0.convert_quiz_duration_to_minutes
lms.patches.v2_0.allow_guest_access #05-02-2025 lms.patches.v2_0.allow_guest_access #05-02-2025
lms.patches.v2_0.migrate_batch_student_data #10-02-2025 lms.patches.v2_0.migrate_batch_student_data #10-02-2025
lms.patches.v2_0.delete_old_enrollment_doctypes lms.patches.v2_0.delete_old_enrollment_doctypes
lms.patches.v2_0.delete_unused_custom_fields

View File

@@ -0,0 +1,24 @@
import frappe
def execute():
if "payments" not in frappe.get_installed_apps():
web_form_custom_fields = frappe.get_all(
"Custom Field", {"dt": "Web Form"}, ["name", "fieldname"]
)
unused_fields = [
"currency",
"amount_field",
"amount_based_on_field",
"payment_button_help",
"amount",
"payments_cb",
"payment_button_label",
"payment_gateway",
"payments_tab",
]
for field in web_form_custom_fields:
if field.fieldname in unused_fields:
frappe.delete_doc("Custom Field", field.name)

View File

@@ -6,12 +6,6 @@
{{ _("We are pleased to inform you that you have been enrolled in our upcoming batch. Congratulations!") }} {{ _("We are pleased to inform you that you have been enrolled in our upcoming batch. Congratulations!") }}
</p> </p>
<br> <br>
<p>
<b>
{{ title }}
</b>
</p>
<p> <p>
<b>{{ _("Batch Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }} <b>{{ _("Batch Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }}
@@ -27,7 +21,7 @@
<br> <br>
<p> <p>
{{ _("Visit the following link to view your ") }} {{ _("Visit the following link to view your ") }}
<a href="/batches/{{ name }}">{{ _("Batch Details") }}</a> <a href="/lms/batches/{{ name }}">{{ _("Batch Details") }}</a>
</p> </p>
<p> <p>
{{ _("If you have any questions or require assistance, feel free to contact us.") }} {{ _("If you have any questions or require assistance, feel free to contact us.") }}

View File

@@ -0,0 +1,35 @@
<p>
{{ _("Dear ") }} {{ student_name }},
</p>
<br>
<p>
{{ _("The batch you have enrolled for is starting tomorrow. Please be prepared and be on time for the session.") }}
</p>
<br>
<p>
<b>{{ _("Batch:") }}</b> {{ title }}
</p>
<br>
<p>
<b>{{ _("Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }}
</p>
<br>
<p>
<b>{{ _("Timings:") }}</b> {{ frappe.utils.format_time(start_time, "hh:mm a") }}
</p>
<br>
<p>
<b>{{ _("Medium:") }}</b> {{ medium }}
</p>
<br>
<p>
{{ _("Visit the following link to view your ") }}
<a href="/lms/batches/{{ name }}">{{ _("Batch Details") }}</a>
</p>
<p>
{{ _("If you have any questions or require assistance, feel free to contact us.") }}
</p>
<br>
<p>
{{ _("Best Regards") }}
</p>

View File

@@ -0,0 +1,31 @@
<p>
{{ _("Dear ") }} {{ student_name }},
</p>
<br>
<p>
{{ _("You have a live class scheduled tomorrow. Please be prepared and be on time for the session.") }}
</p>
<br>
<p>
<b>{{ _("Class:") }}</b> {{ title }}
</p>
<br>
<p>
<b>{{ _("Date:") }}</b> {{ frappe.utils.format_date(date, "medium") }}
</p>
<br>
<p>
<b>{{ _("Timings:") }}</b> {{ frappe.utils.format_time(time, "hh:mm a") }}
</p>
<br>
<p>
{{ _("Visit the following link to view your ") }}
<a href="/lms/live_classes/{{ batch_name }}">{{ _("Batch Details") }}</a>
</p>
<p>
{{ _("If you have any questions or require assistance, feel free to contact us.") }}
</p>
<br>
<p>
{{ _("Best Regards") }}
</p>

View File

@@ -1,18 +1,24 @@
<div> <div>
<p>{{ _('Hi') }} {{ billing_name }},</p> <p>{{ _('Hi') }} {{ billing_name }},</p>
<br>
<p>{{ _('We noticed that you started enrolling in the') }} {{ type }} {{ title }} {{ _('but didnt complete your payment') }}.</p> <p>{{ _('We noticed that you started enrolling in the') }} {{ type }} {{ title }} {{ _('but didnt complete your payment') }}.</p>
<br>
<p> <p>
{{ _("We have a limited number of seats, and they won't be available for long!")}} {{ _("We have a limited number of seats, and they won't be available for long!")}}
</p> </p>
<br>
<p> <p>
{{ _("Dont miss this opportunity to enhance your skills. Click below to complete your enrollment") }}: {{ _("Dont miss this opportunity to enhance your skills. Click below to complete your enrollment") }}:
</p> </p>
<br>
<p> <p>
<a href="{{ link }}">👉 Complete Your Enrollment</a> <a href="{{ link }}">👉 {{ _("Complete Your Enrollment") }}</a>
</p> </p>
<br>
<p> <p>
{{ _("If you have any questions or need assistance, feel free to reach out to our support team.") }} {{ _("If you have any questions or need assistance, feel free to reach out to our support team.") }}
</p> </p>
<br>
<p> <p>
{{ _("Looking forward to seeing you enrolled!") }} {{ _("Looking forward to seeing you enrolled!") }}
</p> </p>

View File

@@ -1,8 +1,9 @@
import frappe import frappe
from frappe.utils.telemetry import capture
from frappe import _
from bs4 import BeautifulSoup
import re import re
from bs4 import BeautifulSoup
from frappe import _
from frappe.utils.telemetry import capture
from frappe.utils import cint
no_cache = 1 no_cache = 1
@@ -17,6 +18,7 @@ def get_context():
csrf_token = frappe.sessions.get_csrf_token() csrf_token = frappe.sessions.get_csrf_token()
frappe.db.commit() # nosemgrep frappe.db.commit() # nosemgrep
context.csrf_token = csrf_token context.csrf_token = csrf_token
context.setup_complete = cint(frappe.get_system_settings("setup_complete"))
capture("active_site", "lms") capture("active_site", "lms")
context.favicon = frappe.db.get_single_value("Website Settings", "favicon") context.favicon = frappe.db.get_single_value("Website Settings", "favicon")
return context return context