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>
window.csrf_token = '{{ csrf_token }}'
window.setup_complete = '{{ setup_complete }}'
document.getElementById('seo-content').style.display = 'none';
</script>
<script type="module" src="/src/main.js"></script>

View File

@@ -62,25 +62,34 @@
</div>
</div>
</div>
<SidebarLink
:link="{
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
}"
:isCollapsed="sidebarStore.isSidebarCollapsed"
@click="toggleSidebar()"
class="m-2"
>
<template #icon>
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
<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>
<TrialBanner
v-if="
userResource.data?.user_type == 'System User' &&
userResource.data?.is_fc_site
"
:isSidebarCollapsed="sidebarStore.isSidebarCollapsed"
/>
<SidebarLink
:link="{
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
}"
:isCollapsed="sidebarStore.isSidebarCollapsed"
@click="toggleSidebar()"
class="m-2"
>
<template #icon>
<span class="grid h-5 w-6 flex-shrink-0 place-items-center">
<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>
<PageModal
v-model="showPageModal"
@@ -101,7 +110,7 @@ import { sessionStore } from '@/stores/session'
import { useSidebar } from '@/stores/sidebar'
import { useSettings } from '@/stores/settings'
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'
const { user, sidebarSettings } = sessionStore()

View File

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

View File

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

View File

@@ -78,7 +78,7 @@
:options="chartOptions"
:series="chartData"
type="bar"
height="200"
:height="chartData[0].data.length * 30 + 100"
/>
<div
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) => {
if (student.assessments[assessment].result === 'Passed') {
if (student.assessments[assessment].result === 'Pass') {
categories[assessment].value += 1
}
})

View File

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

View File

@@ -32,25 +32,44 @@
{{ __('Assessment') }}
</span>
<span>
{{ __('Progress') }}
{{ __('Percentage/Status') }}
</span>
</div>
<div
<router-link
v-for="assessment in Object.keys(student.assessments)"
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">
{{ assessment }}
</span>
<span v-if="isAssignment(student.assessments[assessment])">
<Badge :theme="getStatusTheme(student.assessments[assessment])">
{{ student.assessments[assessment] }}
<span v-if="isAssignment(student.assessments[assessment].status)">
<Badge
:theme="
getStatusTheme(student.assessments[assessment].status)
"
>
{{ student.assessments[assessment].status }}
</Badge>
</span>
<span v-else>
{{ student.assessments[assessment] }}
{{ student.assessments[assessment].status }}
</span>
</div>
</router-link>
</div>
<!-- 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
v-else-if="activeQuestion != questions.length"
@click="nextQuetion()"
@click="nextQuestion()"
>
<span>
{{ __('Next') }}
@@ -258,14 +258,22 @@
</Button>
</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"
>
<ListView
:columns="getSubmissionColumns()"
:rows="attempts?.data"
row-key="name"
:options="{ selectable: false, showTooltip: false }"
:options="{
selectable: false,
showTooltip: false,
emptyState: { title: __('No Quiz submissions found') },
}"
>
</ListView>
</div>
@@ -282,7 +290,7 @@ import {
FormControl,
} from 'frappe-ui'
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 { timeAgo } from '@/utils'
import { useRouter } from 'vue-router'
@@ -536,7 +544,7 @@ const addToLocalStorage = () => {
localStorage.setItem(quiz.data.title, JSON.stringify(quizData))
}
const nextQuetion = () => {
const nextQuestion = () => {
if (!quiz.data.show_answers && questionDetails.data?.type != 'Open Ended') {
checkAnswer()
} else {
@@ -574,6 +582,16 @@ const createSubmission = () => {
if (quiz.data && quiz.data.max_attempts) attempts.reload()
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-model="showSettingsModal"
/>
<FCVerfiyCodeModal v-if="showFCLoginDialog" :email="verificationEmail" />
</template>
<script setup>
import LMSLogo from '@/components/Icons/LMSLogo.vue'
import { sessionStore } from '@/stores/session'
import { Dropdown } from 'frappe-ui'
import { call, Dropdown } from 'frappe-ui'
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 {
ChevronDown,
LogIn,
@@ -74,13 +83,8 @@ import {
User,
Settings,
Sun,
LogInIcon,
} 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 { logout, branding } = sessionStore()
@@ -89,6 +93,11 @@ const settingsStore = useSettings()
let { isLoggedIn } = sessionStore()
const showSettingsModal = ref(false)
const theme = ref('light')
const $dialog = createDialog
const frappeCloudBaseEndpoint = 'https://frappecloud.com'
const showFCLoginDialog = ref(false)
const verificationEmail = ref(null)
const props = defineProps({
isCollapsed: {
@@ -130,6 +139,13 @@ const userDropdownOptions = computed(() => {
return isLoggedIn
},
},
{
icon: theme.value === 'light' ? Moon : Sun,
label: 'Toggle Theme',
onClick: () => {
toggleTheme()
},
},
{
component: markRaw(Apps),
condition: () => {
@@ -139,13 +155,6 @@ const userDropdownOptions = computed(() => {
else return false
},
},
{
icon: theme.value === 'light' ? Moon : Sun,
label: 'Toggle Theme',
onClick: () => {
toggleTheme()
},
},
{
icon: Settings,
label: 'Settings',
@@ -156,6 +165,19 @@ const userDropdownOptions = computed(() => {
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,
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>

View File

@@ -6,7 +6,7 @@
<Breadcrumbs class="h-7" :items="breadcrumbs" />
<div class="flex items-center space-x-2">
<Button
v-if="user.data?.is_moderator"
v-if="user.data?.is_moderator && batch.data?.certification"
@click="openCertificateDialog = true"
>
{{ __('Generate Certificates') }}
@@ -21,7 +21,10 @@
</Button>
</div>
</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">
<Tabs
v-model="tabIndex"
@@ -310,7 +313,7 @@ const tabs = computed(() => {
})
const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/batches`
window.location.href = `/login?redirect-to=/lms/batches/${props.batchName}`
}
const openAnnouncementModal = () => {

View File

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

View File

@@ -13,15 +13,14 @@
<div class="text-lg font-semibold mb-4">
{{ __('Details') }}
</div>
<div class="grid grid-cols-2 gap-10 mb-4 space-y-2">
<div>
<FormControl
v-model="batch.title"
:label="__('Title')"
:required="true"
/>
</div>
<div class="flex flex-col space-y-2">
<div class="space-y-4 mb-4">
<FormControl
v-model="batch.title"
:label="__('Title')"
:required="true"
class="w-full"
/>
<div class="flex items-center space-x-5">
<FormControl
v-model="batch.published"
type="checkbox"
@@ -32,6 +31,11 @@
type="checkbox"
:label="__('Allow self enrollment')"
/>
<FormControl
v-model="batch.certification"
type="checkbox"
:label="__('Certification')"
/>
</div>
</div>
</div>
@@ -90,30 +94,8 @@
:required="true"
:filters="{ ignore_user_type: 1 }"
/>
<div class="mb-4">
<FormControl
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="my-10">
<div class="text-lg font-semibold mb-4">
{{ __('Date and Time') }}
</div>
@@ -133,6 +115,14 @@
class="mb-4"
:required="true"
/>
<FormControl
v-model="batch.timezone"
:label="__('Timezone')"
type="text"
:placeholder="__('Example: IST (+5:30)')"
class="mb-4"
:required="true"
/>
</div>
<div>
<FormControl
@@ -149,18 +139,11 @@
class="mb-4"
: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 class="mb-4">
<div class="mb-10">
<div class="text-lg font-semibold mb-4">
{{ __('Settings') }}
</div>
@@ -179,6 +162,11 @@
type="date"
class="mb-4"
/>
<Link
doctype="Email Template"
:label="__('Email Template')"
v-model="batch.confirmation_email_template"
/>
</div>
<div>
<FormControl
@@ -230,6 +218,33 @@
/>
</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>
</template>
@@ -278,10 +293,12 @@ const batch = reactive({
end_time: '',
timezone: '',
evaluation_end_date: '',
confirmation_email_template: '',
seat_count: '',
medium: '',
category: '',
allow_self_enrollment: false,
certification: false,
image: null,
paid_batch: false,
currency: '',
@@ -351,7 +368,12 @@ const batchDetail = createResource({
batch[key] = `${hours}:${minutes}`
} 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) {
let key = checkboxes[idx]
batch[key] = batch[key] ? true : false

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -951,62 +951,6 @@
"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_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_on_submit": 0,
@@ -1119,230 +1063,6 @@
"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": "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_on_submit": 0,
@@ -1399,174 +1119,6 @@
"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": "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_on_submit": 0,
@@ -1679,62 +1231,6 @@
"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": "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_on_submit": 0,

View File

@@ -116,6 +116,8 @@ scheduler_events = {
"daily": [
"lms.job.doctype.job_opportunity.job_opportunity.update_job_openings",
"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 re
import shutil
import requests
import xml.etree.ElementTree as ET
from frappe.translate import get_all_translations
from frappe import _
from frappe.query_builder import DocType
from frappe.query_builder.functions import Count
from frappe.utils import (
time_diff,
now_datetime,
get_datetime,
cint,
flt,
@@ -24,10 +19,10 @@ from frappe.utils import (
format_date,
date_diff,
)
from typing import Optional
from lms.lms.utils import get_average_rating, get_lesson_count
from xml.dom.minidom import parseString
from lms.lms.doctype.course_lesson.course_lesson import save_progress
from frappe.integrations.frappe_providers.frappecloud_billing import is_fc_site
@frappe.whitelist()
@@ -180,6 +175,7 @@ def get_user_info():
user.is_moderator = "Moderator" in user.roles
user.is_evaluator = "Batch Evaluator" in user.roles
user.is_student = "LMS Student" in user.roles
user.is_fc_site = is_fc_site()
return user
@@ -845,7 +841,7 @@ def update_course_statistics():
@frappe.whitelist()
def get_announcements(batch):
return frappe.get_all(
communications = frappe.get_all(
"Communication",
filters={
"reference_doctype": "LMS Batch",
@@ -863,6 +859,13 @@ def get_announcements(batch):
order_by="communication_date desc",
)
for communication in communications:
communication.image = frappe.get_cached_value(
"User", communication.sender, "user_image"
)
return communications
@frappe.whitelist()
def delete_course(course):
@@ -1225,16 +1228,3 @@ def get_notifications(filters):
@frappe.whitelist(allow_guest=True)
def is_guest_allowed():
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",
"member",
"member_name",
"evaluator",
"section_break_dlzh",
"assignment_attachment",
"answer",
"section_break_ydgh",
"column_break_oqqy",
"evaluator",
"status",
"comments",
"section_break_rqal",
@@ -80,7 +81,7 @@
},
{
"fieldname": "comments",
"fieldtype": "Small Text",
"fieldtype": "Text Editor",
"label": "Comments"
},
{
@@ -139,12 +140,16 @@
{
"fieldname": "column_break_oqqy",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ydgh",
"fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
"modified": "2024-12-24 21:22:35.212732",
"modified": "2025-02-17 18:40:53.374932",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Assignment Submission",

View File

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

View File

@@ -8,7 +8,7 @@ import json
from frappe import _
from datetime import timedelta
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 (
get_lessons,
get_lesson_index,
@@ -405,3 +405,40 @@ def is_milestone_complete(idx, batch):
return False
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):
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(
"LMS Batch",
doc.batch,
["name", "title", "start_date", "start_time", "medium"],
[
"name",
"title",
"start_date",
"start_time",
"medium",
"confirmation_email_template",
],
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 = {
"title": batch.title,
"student_name": doc.member_name,
@@ -107,6 +116,6 @@ def send_mail(doc):
template=template if not custom_template else None,
content=content if custom_template else None,
args=args,
header=[subject, "green"],
header=[_(batch.title), "green"],
retry=3,
)

View File

@@ -2,9 +2,10 @@
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
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):
@@ -56,8 +57,48 @@ class LMSLiveClass(Document):
{
"sync_with_google_calendar": 1,
"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()
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,
"links": [],
"modified": "2025-02-11 14:48:27.801895",
"links": [
{
"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",
"module": "LMS",
"name": "LMS Payment",

View File

@@ -10,12 +10,29 @@ from frappe.desk.doctype.notification_log.notification_log import make_notificat
class LMSQuizSubmission(Document):
def validate(self):
self.validate_if_max_attempts_exceeded()
self.validate_marks()
self.set_percentage()
def on_update(self):
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):
self.score = 0
for row in self.result:
@@ -52,3 +69,7 @@ class LMSQuizSubmission(Document):
)
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)
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(
"LMS Batch",
batch,
@@ -1215,6 +1225,7 @@ def get_batch_details(batch):
"paid_batch",
"evaluation_end_date",
"allow_self_enrollment",
"certification",
"timezone",
"category",
],
@@ -1226,9 +1237,7 @@ def get_batch_details(batch):
batch_details.courses = frappe.get_all(
"Batch Course", filters={"parent": batch}, fields=["course", "title", "evaluator"]
)
batch_details.students = frappe.get_all(
"LMS Batch Enrollment", {"batch": batch}, pluck="member"
)
batch_details.students = batch_students
if batch_details.paid_batch and batch_details.start_date >= getdate():
batch_details.amount, batch_details.currency = check_multicurrency(
@@ -1450,7 +1459,7 @@ def get_batch_students(batch):
)
detail.assessments[title] = assessment_info
if assessment_info.result == "Passed":
if assessment_info.result == "Pass":
assessments_completed += 1
detail.courses_completed = courses_completed
@@ -1493,20 +1502,26 @@ def has_submitted_assessment(assessment, assessment_type, member=None):
attempt = frappe.db.exists(doctype, filters)
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":
result = "Failed"
passing_percentage = frappe.db.get_value(
"LMS Quiz", assessment, "passing_percentage"
)
if attempt_details >= passing_percentage:
result = "Passed"
if attempt_details.percentage >= passing_percentage:
result = "Pass"
else:
result = attempt_details
result = attempt_details.status
return frappe._dict(
{
"status": attempt_details,
"status": attempt_details.percentage
if assessment_type == "LMS Quiz"
else attempt_details.status,
"result": result,
"assessment": assessment,
"type": assessment_type,
"submission": attempt_details.name,
}
)
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

@@ -99,4 +99,5 @@ lms.patches.v2_0.update_quiz_submission_data
lms.patches.v2_0.convert_quiz_duration_to_minutes
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.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!") }}
</p>
<br>
<p>
<b>
{{ title }}
</b>
</p>
<p>
<b>{{ _("Batch Start Date:") }}</b> {{ frappe.utils.format_date(start_date, "medium") }}
@@ -27,7 +21,7 @@
<br>
<p>
{{ _("Visit the following link to view your ") }}
<a href="/batches/{{ name }}">{{ _("Batch Details") }}</a>
<a href="/lms/batches/{{ name }}">{{ _("Batch Details") }}</a>
</p>
<p>
{{ _("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>
<p>{{ _('Hi') }} {{ billing_name }},</p>
<br>
<p>{{ _('We noticed that you started enrolling in the') }} {{ type }} {{ title }} {{ _('but didnt complete your payment') }}.</p>
<br>
<p>
{{ _("We have a limited number of seats, and they won't be available for long!")}}
</p>
<br>
<p>
{{ _("Dont miss this opportunity to enhance your skills. Click below to complete your enrollment") }}:
</p>
<br>
<p>
<a href="{{ link }}">👉 Complete Your Enrollment</a>
<a href="{{ link }}">👉 {{ _("Complete Your Enrollment") }}</a>
</p>
<br>
<p>
{{ _("If you have any questions or need assistance, feel free to reach out to our support team.") }}
</p>
<br>
<p>
{{ _("Looking forward to seeing you enrolled!") }}
</p>

View File

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