Merge pull request #1326 from frappe/develop
chore: merge 'develop' into 'main'
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -62,6 +62,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<TrialBanner
|
||||||
|
v-if="
|
||||||
|
userResource.data?.user_type == 'System User' &&
|
||||||
|
userResource.data?.is_fc_site
|
||||||
|
"
|
||||||
|
:isSidebarCollapsed="sidebarStore.isSidebarCollapsed"
|
||||||
|
/>
|
||||||
<SidebarLink
|
<SidebarLink
|
||||||
:link="{
|
:link="{
|
||||||
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
|
label: sidebarStore.isSidebarCollapsed ? 'Expand' : 'Collapse',
|
||||||
@@ -82,6 +90,7 @@
|
|||||||
</template>
|
</template>
|
||||||
</SidebarLink>
|
</SidebarLink>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<PageModal
|
<PageModal
|
||||||
v-model="showPageModal"
|
v-model="showPageModal"
|
||||||
v-model:reloadSidebar="sidebarSettings"
|
v-model:reloadSidebar="sidebarSettings"
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,16 +155,27 @@
|
|||||||
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>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import {
|
||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
94
frontend/src/components/Modals/FCVerfiyCodeModal.vue
Normal file
94
frontend/src/components/Modals/FCVerfiyCodeModal.vue
Normal 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>
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 = () => {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export default defineConfig({
|
|||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
server: {
|
server: {
|
||||||
allowedHosts: ['fs'],
|
allowedHosts: ['fs', 'bs'],
|
||||||
},
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
__version__ = "2.22.0"
|
__version__ = "2.23.0"
|
||||||
|
|||||||
@@ -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",
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from frappe.utils import (
|
|||||||
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.core.doctype.communication.email import make
|
from frappe.integrations.frappe_providers.frappecloud_billing import is_fc_site
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -175,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
)
|
||||||
|
|||||||
@@ -79,13 +79,20 @@ def send_mail(doc):
|
|||||||
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)
|
subject = _("Enrollment Confirmation for {0}").format(batch.title)
|
||||||
template = "batch_confirmation"
|
template = "batch_confirmation"
|
||||||
custom_template = frappe.db.get_single_value(
|
custom_template = batch.confirmation_email_template or frappe.db.get_single_value(
|
||||||
"LMS Settings", "batch_confirmation_template"
|
"LMS Settings", "batch_confirmation_template"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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"],
|
||||||
|
)
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1194,7 +1194,14 @@ 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):
|
||||||
if not frappe.db.get_value("LMS Batch", batch, "published") and has_student_role():
|
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
|
return
|
||||||
|
|
||||||
batch_details = frappe.db.get_value(
|
batch_details = frappe.db.get_value(
|
||||||
@@ -1218,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",
|
||||||
],
|
],
|
||||||
@@ -1229,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(
|
||||||
|
|||||||
407
lms/locale/ar.po
407
lms/locale/ar.po
File diff suppressed because it is too large
Load Diff
685
lms/locale/bs.po
685
lms/locale/bs.po
File diff suppressed because it is too large
Load Diff
409
lms/locale/de.po
409
lms/locale/de.po
File diff suppressed because it is too large
Load Diff
411
lms/locale/eo.po
411
lms/locale/eo.po
File diff suppressed because it is too large
Load Diff
411
lms/locale/es.po
411
lms/locale/es.po
File diff suppressed because it is too large
Load Diff
417
lms/locale/fa.po
417
lms/locale/fa.po
File diff suppressed because it is too large
Load Diff
407
lms/locale/fr.po
407
lms/locale/fr.po
File diff suppressed because it is too large
Load Diff
407
lms/locale/hu.po
407
lms/locale/hu.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
407
lms/locale/pl.po
407
lms/locale/pl.po
File diff suppressed because it is too large
Load Diff
409
lms/locale/ru.po
409
lms/locale/ru.po
File diff suppressed because it is too large
Load Diff
413
lms/locale/sv.po
413
lms/locale/sv.po
File diff suppressed because it is too large
Load Diff
409
lms/locale/tr.po
409
lms/locale/tr.po
File diff suppressed because it is too large
Load Diff
407
lms/locale/zh.po
407
lms/locale/zh.po
File diff suppressed because it is too large
Load Diff
35
lms/templates/emails/batch_start_reminder.html
Normal file
35
lms/templates/emails/batch_start_reminder.html
Normal 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>
|
||||||
31
lms/templates/emails/live_class_reminder.html
Normal file
31
lms/templates/emails/live_class_reminder.html
Normal 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>
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user