mirror of
https://github.com/frappe/lms.git
synced 2026-05-02 13:39:31 +03:00
Merge pull request #2035 from pateljannat/issues-176
fix: api permissions
This commit is contained in:
@@ -26,8 +26,8 @@
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col overflow-y-auto">
|
||||
<div class="p-5">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="p-5 space-y-5">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="font-semibold text-ink-gray-9">
|
||||
{{ __('Submission') }}
|
||||
</div>
|
||||
@@ -53,7 +53,7 @@
|
||||
!['Pass', 'Fail'].includes(submissionResource.doc?.status) &&
|
||||
submissionResource.doc?.owner == user.data?.name
|
||||
"
|
||||
class="bg-surface-blue-2 text-ink-blue-2 p-3 rounded-md leading-5 text-sm mb-4"
|
||||
class="bg-surface-blue-2 text-ink-blue-2 p-3 rounded-md leading-5 text-sm"
|
||||
>
|
||||
{{ __("You've successfully submitted the assignment.") }}
|
||||
{{
|
||||
@@ -63,12 +63,17 @@
|
||||
}}
|
||||
{{ __('Feel free to make edits to your submission if needed.') }}
|
||||
</div>
|
||||
<div v-if="showUploader()">
|
||||
<div class="text-xs text-ink-gray-5 mt-1 mb-2">
|
||||
{{ __('Add your assignment as {0}').format(assignment.data.type) }}
|
||||
<div v-if="showUploader()" class="border rounded-lg p-3">
|
||||
<div class="font-semibold mb-2">
|
||||
{{ __('Upload Assignment') }}
|
||||
</div>
|
||||
<div class="text-ink-gray-5 text-sm mt-1 mb-4">
|
||||
{{
|
||||
__('You can only upload {0} files').format(assignment.data.type)
|
||||
}}
|
||||
</div>
|
||||
<FileUploader
|
||||
v-if="!submissionFile"
|
||||
v-if="!submissionResource.doc?.assignment_attachment"
|
||||
:fileTypes="getType()"
|
||||
:uploadArgs="{
|
||||
private: true,
|
||||
@@ -87,21 +92,24 @@
|
||||
</template>
|
||||
</FileUploader>
|
||||
<div v-else>
|
||||
<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>
|
||||
<div class="flex items-center text-ink-gray-7">
|
||||
<a
|
||||
:href="submissionFile.file_url"
|
||||
:href="submissionResource.doc.assignment_attachment"
|
||||
target="_blank"
|
||||
class="flex flex-col cursor-pointer !no-underline"
|
||||
class="cursor-pointer !no-underline text-sm leading-5"
|
||||
>
|
||||
<span class="text-sm leading-5">
|
||||
{{ submissionFile.file_name }}
|
||||
</span>
|
||||
<span class="text-sm text-ink-gray-5 mt-1">
|
||||
{{ getFileSize(submissionFile.file_size) }}
|
||||
</span>
|
||||
<div class="flex items-center">
|
||||
<div class="border rounded-md p-2 mr-2">
|
||||
<FileText class="h-5 w-5 stroke-1.5" />
|
||||
</div>
|
||||
<span>
|
||||
{{
|
||||
submissionResource.doc.assignment_attachment
|
||||
.split('/')
|
||||
.pop()
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
<X
|
||||
v-if="canModifyAssignment"
|
||||
@@ -142,13 +150,13 @@
|
||||
user.data?.name == submissionResource.doc?.owner &&
|
||||
submissionResource.doc?.comments
|
||||
"
|
||||
class="mt-8 p-3 bg-surface-blue-2 rounded-md"
|
||||
class="mt-8 p-3 border rounded-lg"
|
||||
>
|
||||
<div class="text-sm text-ink-gray-5 font-medium mb-2">
|
||||
{{ __('Comments by Evaluator') }}:
|
||||
<div class="text-ink-gray-5 mb-4">
|
||||
{{ __('Comments by Evaluator') }}
|
||||
</div>
|
||||
<div
|
||||
class="leading-5 text-ink-gray-9"
|
||||
class="leading-6 text-ink-gray-9"
|
||||
v-html="submissionResource.doc.comments"
|
||||
></div>
|
||||
</div>
|
||||
@@ -204,10 +212,8 @@ import {
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
|
||||
import { FileText, X } from 'lucide-vue-next'
|
||||
import { getFileSize } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const submissionFile = ref(null)
|
||||
const answer = ref(null)
|
||||
const comments = ref(null)
|
||||
const router = useRouter()
|
||||
@@ -266,9 +272,7 @@ const newSubmission = createResource({
|
||||
assignment: props.assignmentID,
|
||||
member: user.data?.name,
|
||||
}
|
||||
if (showUploader()) {
|
||||
doc.assignment_attachment = submissionFile.value.file_url
|
||||
} else {
|
||||
if (!showUploader()) {
|
||||
doc.answer = answer.value
|
||||
}
|
||||
return {
|
||||
@@ -277,19 +281,6 @@ const newSubmission = createResource({
|
||||
},
|
||||
})
|
||||
|
||||
const imageResource = createResource({
|
||||
url: 'lms.lms.api.get_file_info',
|
||||
makeParams(values) {
|
||||
return {
|
||||
file_url: values.image,
|
||||
}
|
||||
},
|
||||
auto: false,
|
||||
onSuccess(data) {
|
||||
submissionFile.value = data
|
||||
},
|
||||
})
|
||||
|
||||
const submissionResource = createDocumentResource({
|
||||
doctype: 'LMS Assignment Submission',
|
||||
name: props.submissionName,
|
||||
@@ -302,11 +293,6 @@ const submissionResource = createDocumentResource({
|
||||
|
||||
watch(submissionResource, () => {
|
||||
if (submissionResource.doc) {
|
||||
if (submissionResource.doc.assignment_attachment) {
|
||||
imageResource.reload({
|
||||
image: submissionResource.doc.assignment_attachment,
|
||||
})
|
||||
}
|
||||
if (submissionResource.doc.answer) {
|
||||
answer.value = submissionResource.doc.answer
|
||||
}
|
||||
@@ -315,7 +301,10 @@ watch(submissionResource, () => {
|
||||
}
|
||||
if (submissionResource.isDirty) {
|
||||
isDirty.value = true
|
||||
} else if (showUploader() && !submissionFile.value) {
|
||||
} else if (
|
||||
showUploader() &&
|
||||
!submissionResource.doc.assignment_attachment
|
||||
) {
|
||||
isDirty.value = true
|
||||
} else if (!showUploader() && !answer.value) {
|
||||
isDirty.value = true
|
||||
@@ -325,11 +314,17 @@ watch(submissionResource, () => {
|
||||
}
|
||||
})
|
||||
|
||||
watch(submissionFile, () => {
|
||||
if (props.submissionName == 'new' && submissionFile.value) {
|
||||
isDirty.value = true
|
||||
watch(
|
||||
() => submissionResource.doc,
|
||||
() => {
|
||||
if (
|
||||
props.submissionName == 'new' &&
|
||||
submissionResource.doc?.assignment_attachment
|
||||
) {
|
||||
isDirty.value = true
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
const submitAssignment = () => {
|
||||
if (props.submissionName != 'new') {
|
||||
@@ -341,13 +336,13 @@ const submitAssignment = () => {
|
||||
submissionResource.setValue.submit(
|
||||
{
|
||||
...submissionResource.doc,
|
||||
assignment_attachment: submissionFile.value?.file_url,
|
||||
evaluator: evaluator,
|
||||
comments: comments.value,
|
||||
answer: answer.value,
|
||||
},
|
||||
{
|
||||
onSuccess(data) {
|
||||
isDirty.value = false
|
||||
toast.success(__('Changes saved successfully'))
|
||||
},
|
||||
}
|
||||
@@ -388,7 +383,7 @@ const addNewSubmission = () => {
|
||||
|
||||
const saveSubmission = (file) => {
|
||||
isDirty.value = true
|
||||
submissionFile.value = file
|
||||
submissionResource.doc.assignment_attachment = file.file_url
|
||||
}
|
||||
|
||||
const markLessonProgress = () => {
|
||||
@@ -439,7 +434,7 @@ const validateFile = (file) => {
|
||||
|
||||
const removeSubmission = () => {
|
||||
isDirty.value = true
|
||||
submissionFile.value = null
|
||||
submissionResource.doc.assignment_attachment = ''
|
||||
}
|
||||
|
||||
const canGradeSubmission = computed(() => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<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"
|
||||
v-if="batch.data.seat_count && batch.data.seats_left > 0"
|
||||
class="text-sm bg-green-100 text-green-700 px-2 py-1 rounded-md"
|
||||
:class="
|
||||
batch.data.amount || batch.data.courses.length
|
||||
@@ -9,16 +9,16 @@
|
||||
: 'w-fit mb-4'
|
||||
"
|
||||
>
|
||||
{{ seats_left }}
|
||||
<span v-if="seats_left > 1">
|
||||
{{ batch.data.seats_left }}
|
||||
<span v-if="batch.data.seats_left > 1">
|
||||
{{ __('Seats Left') }}
|
||||
</span>
|
||||
<span v-else-if="seats_left == 1">
|
||||
<span v-else-if="batch.data.seats_left == 1">
|
||||
{{ __('Seat Left') }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-else-if="batch.data.seat_count && seats_left <= 0"
|
||||
v-else-if="batch.data.seat_count && batch.data.seats_left <= 0"
|
||||
class="text-xs bg-red-100 text-red-700 float-right px-2 py-0.5 rounded-md"
|
||||
>
|
||||
{{ __('Sold Out') }}
|
||||
@@ -54,6 +54,7 @@
|
||||
{{ batch.data.timezone }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!readOnlyMode">
|
||||
<router-link
|
||||
v-if="canAccessBatch"
|
||||
@@ -190,15 +191,10 @@ const enrollInBatch = () => {
|
||||
)
|
||||
}
|
||||
|
||||
const seats_left = computed(() => {
|
||||
if (props.batch.data?.seat_count) {
|
||||
return props.batch.data?.seat_count - props.batch.data?.students?.length
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const isStudent = computed(() => {
|
||||
return props.batch.data?.students?.includes(user.data?.name)
|
||||
return user.data
|
||||
? props.batch.data?.students?.includes(user.data?.name)
|
||||
: false
|
||||
})
|
||||
|
||||
const isModerator = computed(() => {
|
||||
@@ -218,6 +214,9 @@ const isInstructor = computed(() => {
|
||||
})
|
||||
|
||||
const canAccessBatch = computed(() => {
|
||||
if (!user.data) {
|
||||
return false
|
||||
}
|
||||
return isModerator.value || isStudent.value || isEvaluator.value
|
||||
})
|
||||
|
||||
|
||||
@@ -34,7 +34,12 @@
|
||||
<img
|
||||
v-if="type == 'image'"
|
||||
:src="modelValue"
|
||||
class="border rounded-md w-44 h-auto min-h-20 object-cover"
|
||||
:class="[
|
||||
'border object-cover',
|
||||
shape === 'circle'
|
||||
? 'w-20 h-20 rounded-full'
|
||||
: 'w-44 h-auto min-h-20 rounded-md',
|
||||
]"
|
||||
/>
|
||||
<video v-else controls class="border rounded-md w-44 h-auto">
|
||||
<source :src="modelValue" />
|
||||
@@ -72,6 +77,7 @@ const props = withDefaults(
|
||||
description?: string
|
||||
type?: 'image' | 'video'
|
||||
required?: boolean
|
||||
shape?: 'square' | 'circle'
|
||||
}>(),
|
||||
{
|
||||
modelValue: '',
|
||||
@@ -79,6 +85,7 @@ const props = withDefaults(
|
||||
description: '',
|
||||
type: 'image',
|
||||
required: true,
|
||||
shape: 'square',
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -59,6 +59,7 @@
|
||||
v-else-if="course.data.disable_self_learning && !isAdmin"
|
||||
theme="blue"
|
||||
size="lg"
|
||||
class="mb-4"
|
||||
>
|
||||
{{ __('Contact the Administrator to enroll for this course') }}
|
||||
</Badge>
|
||||
|
||||
@@ -1,17 +1,25 @@
|
||||
<template>
|
||||
<Dialog
|
||||
v-model="show"
|
||||
:options="{
|
||||
size: '3xl',
|
||||
}"
|
||||
>
|
||||
<template #body-header>
|
||||
<div class="flex items-center mb-5">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<div class="text-2xl font-semibold leading-6 text-ink-gray-9">
|
||||
{{ __('Edit Profile') }}
|
||||
</div>
|
||||
<Badge v-if="isDirty" class="ml-4" theme="orange">
|
||||
{{ __('Not Saved') }}
|
||||
</Badge>
|
||||
<div class="space-x-2">
|
||||
<Badge v-if="isDirty" theme="orange">
|
||||
{{ __('Not Saved') }}
|
||||
</Badge>
|
||||
<div class="pb-5 float-right">
|
||||
<Button variant="solid" @click="saveProfile()">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #body-content>
|
||||
@@ -19,52 +27,13 @@
|
||||
<div class="grid grid-cols-2 gap-10">
|
||||
<div class="space-y-4">
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<div class="text-xs text-ink-gray-5 mb-1">
|
||||
{{ __('Profile Image') }}
|
||||
</div>
|
||||
<FileUploader
|
||||
v-if="!profile.image"
|
||||
:fileTypes="['image/*']"
|
||||
:validateFile="validateFile"
|
||||
@success="(file) => saveImage(file)"
|
||||
>
|
||||
<template
|
||||
v-slot="{ file, progress, uploading, openFileSelector }"
|
||||
>
|
||||
<div class="mb-4">
|
||||
<Button @click="openFileSelector" :loading="uploading">
|
||||
{{
|
||||
uploading
|
||||
? `Uploading ${progress}%`
|
||||
: 'Upload a profile image'
|
||||
}}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</FileUploader>
|
||||
<div v-else class="mb-4">
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
:src="profile.image?.file_url"
|
||||
class="object-cover h-[50px] w-[50px] rounded-full border-4 border-white object-cover"
|
||||
/>
|
||||
<Uploader
|
||||
v-model="profile.image"
|
||||
:label="__('Profile Image')"
|
||||
:required="true"
|
||||
shape="circle"
|
||||
/>
|
||||
|
||||
<div class="text-base flex flex-col ml-2">
|
||||
<span>
|
||||
{{ profile.image?.file_name }}
|
||||
</span>
|
||||
<span class="text-sm text-ink-gray-4 mt-1">
|
||||
{{ getFileSize(profile.image?.file_size) }}
|
||||
</span>
|
||||
</div>
|
||||
<X
|
||||
@click="removeImage()"
|
||||
class="bg-surface-gray-3 rounded-md cursor-pointer stroke-1.5 w-5 h-5 p-1 ml-4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FormControl
|
||||
v-model="profile.first_name"
|
||||
:label="__('First Name')"
|
||||
@@ -115,13 +84,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #actions="{ close }">
|
||||
<div class="pb-5 float-right">
|
||||
<Button variant="solid" @click="saveProfile(close)">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
@@ -131,15 +93,14 @@ import {
|
||||
createResource,
|
||||
Dialog,
|
||||
FormControl,
|
||||
FileUploader,
|
||||
TextEditor,
|
||||
toast,
|
||||
} from 'frappe-ui'
|
||||
import { ref, reactive, watch } from 'vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { getFileSize, sanitizeHTML } from '@/utils'
|
||||
import { sanitizeHTML } from '@/utils'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
const show = defineModel()
|
||||
const reloadProfile = defineModel('reloadProfile')
|
||||
const hasLanguageChanged = ref(false)
|
||||
const isDirty = ref(false)
|
||||
@@ -163,19 +124,6 @@ const profile = reactive({
|
||||
twitter: '',
|
||||
})
|
||||
|
||||
const imageResource = createResource({
|
||||
url: 'lms.lms.api.get_file_info',
|
||||
makeParams(values) {
|
||||
return {
|
||||
file_url: values.image,
|
||||
}
|
||||
},
|
||||
auto: false,
|
||||
onSuccess(data) {
|
||||
profile.image = data
|
||||
},
|
||||
})
|
||||
|
||||
const updateProfile = createResource({
|
||||
url: 'frappe.client.set_value',
|
||||
makeParams(values) {
|
||||
@@ -183,7 +131,7 @@ const updateProfile = createResource({
|
||||
doctype: 'User',
|
||||
name: props.profile.data.name,
|
||||
fieldname: {
|
||||
user_image: profile.image?.file_url || null,
|
||||
user_image: profile.image || null,
|
||||
...profile,
|
||||
},
|
||||
}
|
||||
@@ -193,13 +141,13 @@ const updateProfile = createResource({
|
||||
},
|
||||
})
|
||||
|
||||
const saveProfile = (close) => {
|
||||
const saveProfile = () => {
|
||||
profile.bio = sanitizeHTML(profile.bio)
|
||||
updateProfile.submit(
|
||||
{},
|
||||
{
|
||||
onSuccess() {
|
||||
close()
|
||||
show.value = false
|
||||
reloadProfile.value.reload()
|
||||
if (hasLanguageChanged.value) {
|
||||
hasLanguageChanged.value = false
|
||||
@@ -213,21 +161,6 @@ const saveProfile = (close) => {
|
||||
)
|
||||
}
|
||||
|
||||
const validateFile = (file) => {
|
||||
let extension = file.name.split('.').pop().toLowerCase()
|
||||
if (!['jpg', 'jpeg', 'png'].includes(extension)) {
|
||||
return 'Only image file is allowed.'
|
||||
}
|
||||
}
|
||||
|
||||
const saveImage = (file) => {
|
||||
profile.image = file
|
||||
}
|
||||
|
||||
const removeImage = () => {
|
||||
profile.image = null
|
||||
}
|
||||
|
||||
watch(
|
||||
() => profile,
|
||||
(newVal) => {
|
||||
@@ -240,7 +173,7 @@ watch(
|
||||
return
|
||||
}
|
||||
}
|
||||
if (profile.image?.file_url !== props.profile.data.user_image) {
|
||||
if (profile.image !== props.profile.data.user_image) {
|
||||
isDirty.value = true
|
||||
return
|
||||
}
|
||||
@@ -262,7 +195,7 @@ watch(
|
||||
profile.linkedin = newVal.linkedin
|
||||
profile.github = newVal.github
|
||||
profile.twitter = newVal.twitter
|
||||
if (newVal.user_image) imageResource.submit({ image: newVal.user_image })
|
||||
profile.image = newVal.user_image
|
||||
isDirty.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -186,8 +186,9 @@ const openProfile = (username: string) => {
|
||||
}
|
||||
|
||||
const deleteEvaluator = (evaluator: string) => {
|
||||
call('lms.lms.api.delete_evaluator', {
|
||||
evaluator: evaluator,
|
||||
call('frappe.client.delete', {
|
||||
doctype: 'Course Evaluator',
|
||||
name: evaluator,
|
||||
})
|
||||
.then(() => {
|
||||
toast.success(__('Evaluator deleted successfully'))
|
||||
|
||||
@@ -269,12 +269,13 @@ const iconProps = {
|
||||
onMounted(() => {
|
||||
setUpOnboarding()
|
||||
addKeyboardShortcut()
|
||||
updateSidebarLinks()
|
||||
socket.on('publish_lms_notifications', (data) => {
|
||||
unreadNotifications.reload()
|
||||
})
|
||||
})
|
||||
|
||||
const setSidebarLinks = () => {
|
||||
const updateSidebarLinksVisibility = () => {
|
||||
sidebarSettings.reload(
|
||||
{},
|
||||
{
|
||||
@@ -596,10 +597,18 @@ watch(userResource, async () => {
|
||||
await programs.reload()
|
||||
setUpOnboarding()
|
||||
}
|
||||
sidebarLinks.value = getSidebarLinks()
|
||||
setSidebarLinks()
|
||||
updateSidebarLinks()
|
||||
})
|
||||
|
||||
watch(settingsStore.settings, () => {
|
||||
updateSidebarLinks()
|
||||
})
|
||||
|
||||
const updateSidebarLinks = () => {
|
||||
sidebarLinks.value = getSidebarLinks()
|
||||
updateSidebarLinksVisibility()
|
||||
}
|
||||
|
||||
const redirectToWebsite = () => {
|
||||
window.open('https://frappe.io/learning', '_blank')
|
||||
}
|
||||
|
||||
@@ -134,6 +134,7 @@ import {
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { GraduationCap } from 'lucide-vue-next'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
|
||||
@@ -145,8 +146,14 @@ const hiring = ref(false)
|
||||
const { brand } = sessionStore()
|
||||
const memberCount = ref(0)
|
||||
const dayjs = inject('$dayjs')
|
||||
const user = inject('$user')
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
if (!user.data) {
|
||||
router.push({ name: 'Courses' })
|
||||
return
|
||||
}
|
||||
setFiltersFromQuery()
|
||||
updateParticipants()
|
||||
})
|
||||
@@ -171,7 +178,7 @@ const categories = createListResource({
|
||||
doctype: 'LMS Certificate',
|
||||
url: 'lms.lms.api.get_certification_categories',
|
||||
cache: ['certification_categories'],
|
||||
auto: true,
|
||||
auto: user.data ? true : false,
|
||||
transform(data) {
|
||||
data.unshift({ label: __(' '), value: ' ' })
|
||||
return data
|
||||
|
||||
@@ -326,6 +326,7 @@
|
||||
@updateNotes="updateNotes"
|
||||
/>
|
||||
<VideoStatistics
|
||||
v-if="showStatsDialog"
|
||||
v-model="showStatsDialog"
|
||||
:lessonName="lesson.data?.name"
|
||||
:lessonTitle="lesson.data?.title"
|
||||
@@ -870,6 +871,7 @@ const scrollDiscussionsIntoView = () => {
|
||||
}
|
||||
|
||||
const updateNotes = () => {
|
||||
if (!user.data) return
|
||||
notes.update({
|
||||
filters: {
|
||||
lesson: lesson.data?.name,
|
||||
|
||||
@@ -185,10 +185,9 @@ const unReadNotifications = createListResource({
|
||||
doctype: 'Notification Log',
|
||||
url: 'lms.lms.api.get_notifications',
|
||||
filters: {
|
||||
for_user: user.data?.name,
|
||||
read: 0,
|
||||
},
|
||||
auto: true,
|
||||
auto: user.data ? true : false,
|
||||
cache: 'Unread Notifications',
|
||||
})
|
||||
|
||||
@@ -196,18 +195,17 @@ const readNotifications = createListResource({
|
||||
doctype: 'Notification Log',
|
||||
url: 'lms.lms.api.get_notifications',
|
||||
filters: {
|
||||
for_user: user.data?.name,
|
||||
read: 1,
|
||||
},
|
||||
auto: true,
|
||||
auto: user.data ? true : false,
|
||||
cache: 'Read Notifications',
|
||||
})
|
||||
|
||||
const markAsRead = createResource({
|
||||
url: 'lms.lms.api.mark_as_read',
|
||||
url: 'frappe.desk.doctype.notification_log.notification_log.mark_as_read',
|
||||
makeParams(values) {
|
||||
return {
|
||||
name: values.name,
|
||||
docname: values.name,
|
||||
}
|
||||
},
|
||||
onSuccess(data) {
|
||||
@@ -217,7 +215,7 @@ const markAsRead = createResource({
|
||||
})
|
||||
|
||||
const markAllAsRead = createResource({
|
||||
url: 'lms.lms.api.mark_all_as_read',
|
||||
url: 'frappe.desk.doctype.notification_log.notification_log.mark_all_as_read',
|
||||
onSuccess(data) {
|
||||
unReadNotifications.reload()
|
||||
readNotifications.reload()
|
||||
|
||||
@@ -13,6 +13,8 @@ export const sessionStore = defineStore('lms-session', () => {
|
||||
let _sessionUser = cookies.get('user_id')
|
||||
if (_sessionUser === 'Guest') {
|
||||
_sessionUser = null
|
||||
} else {
|
||||
userResource.reload()
|
||||
}
|
||||
return _sessionUser
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ export const usersStore = defineStore('lms-users', () => {
|
||||
window.location.href = '/login'
|
||||
}
|
||||
},
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const allUsers = createResource({
|
||||
|
||||
@@ -489,6 +489,9 @@ const getSidebarItems = () => {
|
||||
icon: 'GraduationCap',
|
||||
to: 'CertifiedParticipants',
|
||||
activeFor: ['CertifiedParticipants'],
|
||||
condition: () => {
|
||||
return userResource?.data
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Jobs',
|
||||
|
||||
@@ -198,7 +198,6 @@ update_website_context = [
|
||||
|
||||
jinja = {
|
||||
"methods": [
|
||||
"lms.lms.utils.get_tags",
|
||||
"lms.lms.utils.get_lesson_count",
|
||||
"lms.lms.utils.get_instructors",
|
||||
"lms.lms.utils.get_lesson_index",
|
||||
|
||||
+84
-112
@@ -31,15 +31,20 @@ from pypika import functions as fn
|
||||
|
||||
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
||||
from lms.lms.utils import (
|
||||
can_modify_batch,
|
||||
can_modify_course,
|
||||
get_average_rating,
|
||||
get_batch_details,
|
||||
get_course_details,
|
||||
get_instructors,
|
||||
get_lesson_count,
|
||||
has_course_instructor_role,
|
||||
has_evaluator_role,
|
||||
has_moderator_role,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def get_user_info():
|
||||
if frappe.session.user == "Guest":
|
||||
return None
|
||||
@@ -223,7 +228,6 @@ def get_chart_details():
|
||||
return details
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_file_info(file_url):
|
||||
"""Get file info for the given file URL."""
|
||||
file_info = frappe.db.get_value(
|
||||
@@ -235,17 +239,20 @@ def get_file_info(file_url):
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_branding():
|
||||
"""Get branding details."""
|
||||
website_settings = frappe.get_single("Website Settings")
|
||||
image_fields = ["banner_image", "footer_logo", "favicon"]
|
||||
fields = ["app_name"]
|
||||
image_fields = ["banner_image", "footer_logo", "favicon", "app_logo"]
|
||||
fields = fields + image_fields
|
||||
settings = frappe._dict()
|
||||
|
||||
for field in image_fields:
|
||||
if website_settings.get(field):
|
||||
file_info = get_file_info(website_settings.get(field))
|
||||
website_settings.update({field: json.loads(json.dumps(file_info))})
|
||||
for field in fields:
|
||||
value = frappe.get_cached_value("Website Settings", None, field)
|
||||
if field in image_fields and value:
|
||||
file_info = get_file_info(value)
|
||||
settings.update({field: json.loads(json.dumps(file_info))})
|
||||
else:
|
||||
website_settings.update({field: None})
|
||||
settings.update({field: value})
|
||||
|
||||
return website_settings
|
||||
return settings
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -285,7 +292,7 @@ def get_evaluator_details(evaluator):
|
||||
}
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def get_certified_participants(filters=None, start=0, page_length=100):
|
||||
query = get_certification_query(filters)
|
||||
query = query.orderby("issue_date", order=frappe.qb.desc).offset(start).limit(page_length)
|
||||
@@ -339,14 +346,14 @@ def get_certification_query(filters):
|
||||
return query
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def get_count_of_certified_members(filters=None):
|
||||
query = get_certification_query(filters)
|
||||
result = query.run(as_dict=True)
|
||||
return len(result) or 0
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@frappe.whitelist()
|
||||
def get_certification_categories():
|
||||
categories = []
|
||||
seen = set()
|
||||
@@ -368,20 +375,6 @@ def get_certification_categories():
|
||||
return categories
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_assigned_badges(member):
|
||||
assigned_badges = frappe.get_all(
|
||||
"LMS Badge Assignment",
|
||||
{"member": member},
|
||||
["badge"],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for badge in assigned_badges:
|
||||
badge.update(frappe.db.get_value("LMS Badge", badge.badge, ["name", "title", "image"]))
|
||||
return assigned_badges
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_all_users():
|
||||
frappe.only_for(["Moderator", "Course Creator", "Batch Evaluator"])
|
||||
@@ -396,28 +389,13 @@ def get_all_users():
|
||||
return {user.name: user for user in users}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def mark_as_read(name):
|
||||
doc = frappe.get_doc("Notification Log", name)
|
||||
doc.read = 1
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def mark_all_as_read():
|
||||
notifications = frappe.get_all(
|
||||
"Notification Log", {"for_user": frappe.session.user, "read": 0}, pluck="name"
|
||||
)
|
||||
|
||||
for notification in notifications:
|
||||
mark_as_read(notification)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_sidebar_settings():
|
||||
lms_settings = frappe.get_single("LMS Settings")
|
||||
sidebar_items = frappe._dict()
|
||||
if not lms_settings.allow_guest_access:
|
||||
return []
|
||||
|
||||
sidebar_items = frappe._dict()
|
||||
items = [
|
||||
"courses",
|
||||
"batches",
|
||||
@@ -446,6 +424,7 @@ def get_sidebar_settings():
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_sidebar_item(webpage, icon):
|
||||
frappe.only_for("Moderator")
|
||||
filters = {
|
||||
"web_page": webpage,
|
||||
"parenttype": "LMS Settings",
|
||||
@@ -464,6 +443,7 @@ def update_sidebar_item(webpage, icon):
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_sidebar_item(webpage):
|
||||
frappe.only_for("Moderator")
|
||||
return frappe.db.delete(
|
||||
"LMS Sidebar Item",
|
||||
{
|
||||
@@ -477,6 +457,10 @@ def delete_sidebar_item(webpage):
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_lesson(lesson, chapter):
|
||||
course = frappe.db.get_value("Course Chapter", chapter, "course")
|
||||
if not can_modify_course(course):
|
||||
frappe.throw(_("You do not have permission to delete this lesson."), frappe.PermissionError)
|
||||
|
||||
# Delete Reference
|
||||
chapter = frappe.get_doc("Course Chapter", chapter)
|
||||
chapter.lessons = [row for row in chapter.lessons if row.lesson != lesson]
|
||||
@@ -491,8 +475,11 @@ def delete_lesson(lesson, chapter):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_lesson_index(lesson, sourceChapter, targetChapter, idx):
|
||||
hasMoved = sourceChapter == targetChapter
|
||||
course = frappe.db.get_value("Course Chapter", sourceChapter, "course")
|
||||
if not can_modify_course(course):
|
||||
frappe.throw(_("You do not have permission to modify this lesson."), frappe.PermissionError)
|
||||
|
||||
hasMoved = sourceChapter == targetChapter
|
||||
update_source_chapter(lesson, sourceChapter, idx, hasMoved)
|
||||
if not hasMoved:
|
||||
update_target_chapter(lesson, targetChapter, idx)
|
||||
@@ -551,6 +538,10 @@ def update_index(lessons, chapter):
|
||||
@frappe.whitelist()
|
||||
def update_chapter_index(chapter, course, idx):
|
||||
"""Update the index of a chapter within a course"""
|
||||
|
||||
if not can_modify_course(course):
|
||||
frappe.throw(_("You do not have permission to modify this chapter."), frappe.PermissionError)
|
||||
|
||||
chapters = frappe.get_all(
|
||||
"Chapter Reference",
|
||||
{"parent": course},
|
||||
@@ -567,26 +558,9 @@ def update_chapter_index(chapter, course, idx):
|
||||
frappe.db.set_value("Chapter Reference", {"chapter": chapter_name, "parent": course}, "idx", i + 1)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_categories(doctype, filters):
|
||||
categoryOptions = []
|
||||
|
||||
categories = frappe.get_all(
|
||||
doctype,
|
||||
filters,
|
||||
pluck="category",
|
||||
)
|
||||
categories = list(set(categories))
|
||||
|
||||
for category in categories:
|
||||
if category:
|
||||
categoryOptions.append({"label": category, "value": category})
|
||||
|
||||
return categoryOptions
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_members(start=0, search=""):
|
||||
frappe.only_for(["Moderator"])
|
||||
filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]}
|
||||
or_filters = {}
|
||||
|
||||
@@ -653,6 +627,7 @@ def save_evaluation_details(
|
||||
"""
|
||||
Save evaluation details for a member against a course.
|
||||
"""
|
||||
frappe.only_for(["Batch Evaluator", "Moderator"])
|
||||
evaluation = frappe.db.exists("LMS Certificate Evaluation", {"member": member, "course": course})
|
||||
|
||||
details = {
|
||||
@@ -696,6 +671,7 @@ def save_certificate_details(
|
||||
"""
|
||||
Save certificate details for a member against a course.
|
||||
"""
|
||||
frappe.only_for(["Batch Evaluator", "Moderator"])
|
||||
certificate = frappe.db.exists("LMS Certificate", {"member": member, "course": course})
|
||||
|
||||
details = {
|
||||
@@ -730,16 +706,9 @@ def delete_documents(doctype, documents):
|
||||
frappe.delete_doc(doctype, doc)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_count(doctype, filters):
|
||||
return frappe.db.count(
|
||||
doctype,
|
||||
filters=filters,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_gateway_details(payment_gateway):
|
||||
frappe.only_for("Moderator")
|
||||
gateway = frappe.get_doc("Payment Gateway", payment_gateway)
|
||||
|
||||
if gateway.gateway_controller is None:
|
||||
@@ -795,6 +764,7 @@ def get_transformed_fields(meta, data=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_new_gateway_fields(doctype):
|
||||
frappe.only_for("Moderator")
|
||||
try:
|
||||
meta = frappe.get_meta(doctype).fields
|
||||
except Exception:
|
||||
@@ -825,6 +795,18 @@ def update_course_statistics():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_announcements(batch):
|
||||
roles = frappe.get_roles()
|
||||
is_batch_student = frappe.db.exists(
|
||||
"LMS Batch Enrollment", {"batch": batch, "member": frappe.session.user}
|
||||
)
|
||||
is_moderator = "Moderator" in roles
|
||||
is_evaluator = "Batch Evaluator" in roles
|
||||
|
||||
if not (is_batch_student or is_moderator or is_evaluator):
|
||||
frappe.throw(
|
||||
_("You do not have permission to access announcements for this batch."), frappe.PermissionError
|
||||
)
|
||||
|
||||
communications = frappe.get_all(
|
||||
"Communication",
|
||||
filters={
|
||||
@@ -851,6 +833,9 @@ def get_announcements(batch):
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_course(course):
|
||||
if not can_modify_course(course):
|
||||
frappe.throw(_("You do not have permission to delete this course."), frappe.PermissionError)
|
||||
|
||||
frappe.db.delete("LMS Enrollment", {"course": course})
|
||||
frappe.db.delete("LMS Course Progress", {"course": course})
|
||||
frappe.db.set_value("LMS Quiz", {"course": course}, "course", None)
|
||||
@@ -885,6 +870,9 @@ def delete_course(course):
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_batch(batch):
|
||||
if not can_modify_batch(batch):
|
||||
frappe.throw(_("You do not have permission to delete this batch."), frappe.PermissionError)
|
||||
|
||||
frappe.db.delete("LMS Batch Enrollment", {"batch": batch})
|
||||
frappe.db.delete("Batch Course", {"parent": batch, "parenttype": "LMS Batch"})
|
||||
frappe.db.delete("LMS Assessment", {"parent": batch, "parenttype": "LMS Batch"})
|
||||
@@ -928,6 +916,9 @@ def give_discussions_permission():
|
||||
|
||||
@frappe.whitelist()
|
||||
def upsert_chapter(title, course, is_scorm_package, scorm_package, name=None):
|
||||
if not can_modify_course(course):
|
||||
frappe.throw(_("You do not have permission to modify this chapter."), frappe.PermissionError)
|
||||
|
||||
values = frappe._dict({"title": title, "course": course, "is_scorm_package": is_scorm_package})
|
||||
|
||||
if is_scorm_package:
|
||||
@@ -1050,6 +1041,10 @@ def add_lesson(title, chapter, course, idx):
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_chapter(chapter):
|
||||
course = frappe.db.get_value("Course Chapter", chapter, "course")
|
||||
if not can_modify_course(course):
|
||||
frappe.throw(_("You do not have permission to delete this chapter."), frappe.PermissionError)
|
||||
|
||||
chapterInfo = frappe.db.get_value(
|
||||
"Course Chapter", chapter, ["is_scorm_package", "scorm_package_path"], as_dict=True
|
||||
)
|
||||
@@ -1092,9 +1087,9 @@ def mark_lesson_progress(course, chapter_number, lesson_number):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_heatmap_data(member=None, base_days=200):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
def get_heatmap_data(member, base_days=200):
|
||||
if not (has_course_instructor_role() or has_moderator_role() or has_evaluator_role()):
|
||||
frappe.throw(_("You do not have permission to access heatmap data."), frappe.PermissionError)
|
||||
|
||||
base_date, start_date, number_of_days, days = calculate_date_ranges(base_days)
|
||||
date_count = initialize_date_count(days)
|
||||
@@ -1207,6 +1202,8 @@ def get_week_difference(start_date, current_date):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_notifications(filters):
|
||||
filters = frappe._dict(filters or {})
|
||||
filters.for_user = frappe.session.user
|
||||
notifications = frappe.get_all(
|
||||
"Notification Log",
|
||||
filters,
|
||||
@@ -1306,9 +1303,8 @@ def get_lms_settings():
|
||||
@frappe.whitelist()
|
||||
def cancel_evaluation(evaluation):
|
||||
evaluation = frappe._dict(evaluation)
|
||||
|
||||
if evaluation.member != frappe.session.user:
|
||||
return
|
||||
frappe.throw(_("You do not have permission to cancel this evaluation."), frappe.PermissionError)
|
||||
|
||||
frappe.db.set_value("LMS Certificate Request", evaluation.name, "status", "Cancelled")
|
||||
events = frappe.get_all(
|
||||
@@ -1406,16 +1402,6 @@ def add_an_evaluator(email):
|
||||
return evaluator
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_evaluator(evaluator):
|
||||
frappe.only_for("Moderator")
|
||||
if not frappe.db.exists("Course Evaluator", evaluator):
|
||||
frappe.throw(_("Evaluator does not exist."))
|
||||
|
||||
frappe.db.delete("Has Role", {"parent": evaluator, "role": "Batch Evaluator"})
|
||||
frappe.db.delete("Course Evaluator", evaluator)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def capture_user_persona(responses):
|
||||
frappe.only_for("System Manager")
|
||||
@@ -1448,6 +1434,7 @@ def get_meta_info(type, route):
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_meta_info(meta_type, route, meta_tags):
|
||||
frappe.only_for(["Course Creator", "Batch Evaluator", "Moderator"])
|
||||
validate_meta_data_permissions(meta_type)
|
||||
validate_meta_tags(meta_tags)
|
||||
|
||||
@@ -1548,6 +1535,10 @@ def make_new_exercise_submission(exercise, code, test_cases):
|
||||
|
||||
|
||||
def update_exercise_submission(submission, code, test_cases):
|
||||
member = frappe.db.get_value("LMS Programming Exercise Submission", submission, "member")
|
||||
if member != frappe.session.user:
|
||||
frappe.throw(_("You do not have permission to update this submission."), frappe.PermissionError)
|
||||
|
||||
update_test_cases(test_cases, submission)
|
||||
status = get_exercise_status(test_cases)
|
||||
frappe.db.set_value("LMS Programming Exercise Submission", submission, {"status": status, "code": code})
|
||||
@@ -1620,11 +1611,12 @@ def track_new_watch_time(lesson, video):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_course_progress_distribution(course):
|
||||
roles = frappe.get_roles()
|
||||
if "Course Creator" not in roles and "Moderator" not in roles:
|
||||
frappe.throw(_("You do not have permission to access course progress data."))
|
||||
if not can_modify_course(course):
|
||||
frappe.throw(
|
||||
_("You do not have permission to access this course's progress data."), frappe.PermissionError
|
||||
)
|
||||
|
||||
all_progress = frappe.get_list(
|
||||
all_progress = frappe.get_all(
|
||||
"LMS Enrollment",
|
||||
{
|
||||
"course": course,
|
||||
@@ -1720,9 +1712,6 @@ def get_profile_details(username):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_streak_info():
|
||||
if frappe.session.user == "Guest":
|
||||
return {}
|
||||
|
||||
all_dates = fetch_activity_dates(frappe.session.user)
|
||||
streak, longest_streak = calculate_streaks(all_dates)
|
||||
current_streak = calculate_current_streak(all_dates, streak)
|
||||
@@ -1791,8 +1780,6 @@ def calculate_current_streak(all_dates, streak):
|
||||
@frappe.whitelist()
|
||||
def get_my_live_classes():
|
||||
my_live_classes = []
|
||||
if frappe.session.user == "Guest":
|
||||
return my_live_classes
|
||||
|
||||
batches = frappe.get_all(
|
||||
"LMS Batch Enrollment",
|
||||
@@ -1837,8 +1824,6 @@ def get_my_live_classes():
|
||||
@frappe.whitelist()
|
||||
def get_created_courses():
|
||||
created_courses = []
|
||||
if frappe.session.user == "Guest":
|
||||
return created_courses
|
||||
|
||||
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||
Course = frappe.qb.DocType("LMS Course")
|
||||
@@ -1866,8 +1851,6 @@ def get_created_courses():
|
||||
@frappe.whitelist()
|
||||
def get_created_batches():
|
||||
created_batches = []
|
||||
if frappe.session.user == "Guest":
|
||||
return created_batches
|
||||
|
||||
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||
Batch = frappe.qb.DocType("LMS Batch")
|
||||
@@ -1895,9 +1878,6 @@ def get_created_batches():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_admin_live_classes():
|
||||
if frappe.session.user == "Guest":
|
||||
return []
|
||||
|
||||
CourseInstructor = frappe.qb.DocType("Course Instructor")
|
||||
LMSLiveClass = frappe.qb.DocType("LMS Live Class")
|
||||
|
||||
@@ -1928,9 +1908,6 @@ def get_admin_live_classes():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_admin_evals():
|
||||
if frappe.session.user == "Guest":
|
||||
return []
|
||||
|
||||
evals = frappe.get_all(
|
||||
"LMS Certificate Request",
|
||||
{
|
||||
@@ -1960,9 +1937,6 @@ def get_admin_evals():
|
||||
@frappe.whitelist()
|
||||
def get_my_courses():
|
||||
my_courses = []
|
||||
if frappe.session.user == "Guest":
|
||||
return my_courses
|
||||
|
||||
courses = get_my_latest_courses()
|
||||
|
||||
if not len(courses):
|
||||
@@ -2014,9 +1988,6 @@ def get_popular_courses():
|
||||
@frappe.whitelist()
|
||||
def get_my_batches():
|
||||
my_batches = []
|
||||
if frappe.session.user == "Guest":
|
||||
return my_batches
|
||||
|
||||
batches = get_my_latest_batches()
|
||||
|
||||
if not len(batches):
|
||||
@@ -2057,6 +2028,7 @@ def get_upcoming_batches():
|
||||
|
||||
@frappe.whitelist()
|
||||
def delete_programming_exercise(exercise):
|
||||
frappe.only_for(["Moderator", "Course Creator"])
|
||||
frappe.db.delete("LMS Programming Exercise Submission", {"exercise": exercise})
|
||||
frappe.db.delete("LMS Programming Exercise", exercise)
|
||||
|
||||
|
||||
@@ -182,8 +182,3 @@ def get_assignment_progress(lesson):
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_lesson_info(chapter):
|
||||
return frappe.db.get_value("Course Chapter", chapter, "course")
|
||||
|
||||
@@ -231,32 +231,9 @@ def update_meeting_details(eval, event, calendar):
|
||||
frappe.db.set_value("LMS Certificate Request", eval.name, "google_meet_link", event.google_meet_link)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_certificate_request(course, date, day, start_time, end_time, batch_name=None):
|
||||
is_member = frappe.db.exists(
|
||||
{"doctype": "LMS Enrollment", "course": course, "member": frappe.session.user}
|
||||
)
|
||||
|
||||
if not is_member:
|
||||
return
|
||||
eval = frappe.new_doc("LMS Certificate Request")
|
||||
eval.update(
|
||||
{
|
||||
"course": course,
|
||||
"evaluator": get_evaluator(course, batch_name),
|
||||
"member": frappe.session.user,
|
||||
"date": date,
|
||||
"day": day,
|
||||
"start_time": start_time,
|
||||
"end_time": end_time,
|
||||
"batch_name": batch_name,
|
||||
}
|
||||
)
|
||||
eval.save(ignore_permissions=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_lms_certificate_evaluation(source_name, target_doc=None):
|
||||
frappe.only_for(["Moderator", "Batch Evaluator", "System Manager"])
|
||||
doc = get_mapped_doc(
|
||||
"LMS Certificate Request",
|
||||
source_name,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) 2021, FOSS United and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on("LMS Mentor Request", {
|
||||
// refresh: function(frm) {
|
||||
// }
|
||||
});
|
||||
@@ -1,87 +0,0 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-04-18 11:48:02.635688",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"member",
|
||||
"course",
|
||||
"reviewed_by",
|
||||
"column_break_3",
|
||||
"member_name",
|
||||
"status",
|
||||
"comments"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Member",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.full_name",
|
||||
"fieldname": "member_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Member Name"
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"options": "Pending\nApproved\nRejected\nWithdrawn"
|
||||
},
|
||||
{
|
||||
"fieldname": "reviewed_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Reviewed By",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "comments",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Comments"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-21 11:49:12.543502",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Mentor Request",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSMentorRequest(Document):
|
||||
def on_update(self):
|
||||
if self.has_value_changed("status"):
|
||||
if self.status == "Approved":
|
||||
self.create_course_mentor_mapping()
|
||||
|
||||
if self.status != "Pending":
|
||||
self.send_status_change_email()
|
||||
|
||||
def create_course_mentor_mapping(self):
|
||||
mapping = frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Course Mentor Mapping",
|
||||
"mentor": self.member,
|
||||
"course": self.course,
|
||||
}
|
||||
)
|
||||
mapping.save()
|
||||
|
||||
def send_creation_email(self):
|
||||
email_template = self.get_email_template("mentor_request_creation")
|
||||
if not email_template:
|
||||
return
|
||||
|
||||
course_details = frappe.db.get_value(
|
||||
"LMS Course", self.course, ["owner", "slug", "title"], as_dict=True
|
||||
)
|
||||
message = frappe.render_template(
|
||||
email_template.response,
|
||||
{
|
||||
"member_name": frappe.db.get_value("User", frappe.session.user, "full_name"),
|
||||
"course_url": "/lms/courses/" + course_details.slug,
|
||||
"course": course_details.title,
|
||||
},
|
||||
)
|
||||
|
||||
email_args = {
|
||||
"recipients": [frappe.session.user, course_details.owner],
|
||||
"subject": email_template.subject,
|
||||
"header": email_template.subject,
|
||||
"message": message,
|
||||
}
|
||||
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
|
||||
|
||||
def send_status_change_email(self):
|
||||
email_template = self.get_email_template("mentor_request_status_update")
|
||||
if not email_template:
|
||||
return
|
||||
|
||||
course_details = frappe.db.get_value("LMS Course", self.course, ["owner", "title"], as_dict=True)
|
||||
message = frappe.render_template(
|
||||
email_template.response,
|
||||
{
|
||||
"member_name": self.member_name,
|
||||
"status": self.status,
|
||||
"course": course_details.title,
|
||||
},
|
||||
)
|
||||
|
||||
if self.status == "Approved" or self.status == "Rejected":
|
||||
email_args = {
|
||||
"recipients": self.member,
|
||||
"cc": [course_details.owner, self.reviewed_by],
|
||||
"subject": email_template.subject,
|
||||
"header": email_template.subject,
|
||||
"message": message,
|
||||
}
|
||||
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
|
||||
|
||||
elif self.status == "Withdrawn":
|
||||
email_args = {
|
||||
"recipients": [self.member, course_details.owner],
|
||||
"subject": email_template.subject,
|
||||
"header": email_template.subject,
|
||||
"message": message,
|
||||
}
|
||||
frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
|
||||
|
||||
def get_email_template(self, template_name):
|
||||
template = frappe.db.get_single_value("LMS Settings", template_name)
|
||||
if template:
|
||||
return frappe.get_doc("Email Template", template)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def has_requested(course):
|
||||
return frappe.db.count(
|
||||
"LMS Mentor Request",
|
||||
filters={
|
||||
"member": frappe.session.user,
|
||||
"course": course,
|
||||
"status": ["in", ("Pending", "Approved")],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_request(course):
|
||||
if not has_requested(course):
|
||||
request = frappe.get_doc(
|
||||
{
|
||||
"doctype": "LMS Mentor Request",
|
||||
"member": frappe.session.user,
|
||||
"course": course,
|
||||
"status": "Pending",
|
||||
}
|
||||
)
|
||||
request.save(ignore_permissions=True)
|
||||
request.send_creation_email()
|
||||
return "OK"
|
||||
|
||||
else:
|
||||
return "Already Applied"
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_request(course):
|
||||
request = frappe.get_doc(
|
||||
"LMS Mentor Request",
|
||||
{
|
||||
"member": frappe.session.user,
|
||||
"course": course,
|
||||
"status": ["in", ("Pending", "Approved")],
|
||||
},
|
||||
)
|
||||
request.status = "Withdrawn"
|
||||
request.save(ignore_permissions=True)
|
||||
return "OK"
|
||||
@@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2021, FOSS United and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
|
||||
class TestLMSMentorRequest(unittest.TestCase):
|
||||
pass
|
||||
@@ -16,7 +16,6 @@ from lms.lms.utils import (
|
||||
get_lessons,
|
||||
get_membership,
|
||||
get_reviews,
|
||||
get_tags,
|
||||
has_course_instructor_role,
|
||||
has_evaluator_role,
|
||||
has_moderator_role,
|
||||
@@ -98,11 +97,6 @@ class TestLMSUtils(BaseTestUtils):
|
||||
all_lessons = frappe.db.count("Course Lesson", {"course": self.course.name})
|
||||
self.assertEqual(len(lessons), all_lessons)
|
||||
|
||||
def test_get_tags(self):
|
||||
tags = get_tags(self.course.name)
|
||||
expected_tags = ["Frappe", "Learning", "Utility"]
|
||||
self.assertEqual(set(tags), set(expected_tags))
|
||||
|
||||
def test_get_instructors(self):
|
||||
instructors = get_instructors("LMS Course", self.course.name)
|
||||
self.assertEqual(len(instructors), len(self.course.instructors))
|
||||
|
||||
+95
-20
@@ -202,13 +202,6 @@ def get_lesson_icon(body, content):
|
||||
return "icon-list"
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_tags(course):
|
||||
tags = frappe.db.get_value("LMS Course", course, "tags")
|
||||
return tags.split(",") if tags else []
|
||||
|
||||
|
||||
def get_instructors(doctype, docname):
|
||||
instructor_details = []
|
||||
instructors = frappe.get_all(
|
||||
@@ -254,7 +247,7 @@ def get_reviews(course):
|
||||
for review in reviews:
|
||||
review.rating = review.rating * out_of_ratings
|
||||
review.owner_details = frappe.db.get_value(
|
||||
"User", review.owner, ["name", "username", "full_name", "user_image"], as_dict=True
|
||||
"User", review.owner, ["username", "full_name", "user_image"], as_dict=True
|
||||
)
|
||||
review.creation = pretty_date(review.creation)
|
||||
|
||||
@@ -654,9 +647,6 @@ def get_evaluator(course, batch=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_upcoming_evals(courses=None, batch=None):
|
||||
if frappe.session.user == "Guest":
|
||||
return []
|
||||
|
||||
if not courses:
|
||||
courses = []
|
||||
|
||||
@@ -759,11 +749,21 @@ def get_current_exchange_rate(source, target="USD"):
|
||||
return details["rates"][target]
|
||||
|
||||
|
||||
def guest_access_allowed():
|
||||
allow_guest_access = frappe.get_cached_value("LMS Settings", None, "allow_guest_access")
|
||||
if frappe.session.user == "Guest" and not allow_guest_access:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_courses(filters=None, start=0):
|
||||
"""Returns the list of courses."""
|
||||
|
||||
if not guest_access_allowed():
|
||||
return []
|
||||
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
@@ -905,6 +905,9 @@ def get_course_fields():
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_course_details(course):
|
||||
if not guest_access_allowed():
|
||||
return {}
|
||||
|
||||
fields = get_course_fields()
|
||||
course_details = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
@@ -976,6 +979,10 @@ def get_categorized_courses(courses):
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_course_outline(course, progress=False):
|
||||
"""Returns the course outline."""
|
||||
|
||||
if not guest_access_allowed():
|
||||
return []
|
||||
|
||||
outline = []
|
||||
chapters = frappe.get_all("Chapter Reference", {"parent": course}, ["chapter", "idx"], order_by="idx")
|
||||
for chapter in chapters:
|
||||
@@ -1003,6 +1010,9 @@ def get_course_outline(course, progress=False):
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_lesson(course, chapter, lesson):
|
||||
if not guest_access_allowed():
|
||||
return {}
|
||||
|
||||
chapter_name = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": chapter}, "chapter")
|
||||
lesson_name = frappe.db.get_value("Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson")
|
||||
lesson_details = frappe.db.get_value(
|
||||
@@ -1114,8 +1124,10 @@ def get_neighbour_lesson(course, chapter, lesson):
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_batch_details(batch):
|
||||
batch_students = frappe.get_all("LMS Batch Enrollment", {"batch": batch}, pluck="member")
|
||||
if not guest_access_allowed():
|
||||
return {}
|
||||
|
||||
batch_students = frappe.get_all("LMS Batch Enrollment", {"batch": batch}, pluck="member")
|
||||
has_create_batch_role = can_create_batches()
|
||||
is_course_published = frappe.db.get_value("LMS Batch", batch, "published")
|
||||
is_student_enrolled = frappe.session.user in batch_students
|
||||
@@ -1239,6 +1251,9 @@ def get_question_details(question):
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_batch_courses(batch):
|
||||
if not guest_access_allowed():
|
||||
return []
|
||||
|
||||
courses = []
|
||||
course_list = frappe.get_all("Batch Course", {"parent": batch}, ["name", "course"])
|
||||
|
||||
@@ -1251,10 +1266,8 @@ def get_batch_courses(batch):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_assessments(batch, member=None):
|
||||
if not member:
|
||||
member = frappe.session.user
|
||||
|
||||
def get_assessments(batch):
|
||||
member = frappe.session.user
|
||||
assessments = frappe.get_all(
|
||||
"LMS Assessment",
|
||||
{"parent": batch},
|
||||
@@ -1362,6 +1375,7 @@ def get_exercise_details(assessment, member):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_batch_assessment_count(batch):
|
||||
frappe.only_for(["Moderator", "Batch Evaluator"])
|
||||
if not frappe.db.exists("LMS Batch", batch):
|
||||
frappe.throw(_("The specified batch does not exist."))
|
||||
return frappe.db.count("LMS Assessment", {"parent": batch})
|
||||
@@ -1372,11 +1386,13 @@ def get_batch_students(filters, offset=0, limit_start=0, limit_page_length=None,
|
||||
# limit_start and limit_page_length are used for backward compatibility
|
||||
start = limit_start or offset
|
||||
page_length = limit_page_length or limit
|
||||
|
||||
batch = filters.get("batch")
|
||||
if not batch:
|
||||
return []
|
||||
|
||||
if not can_modify_batch(batch):
|
||||
frappe.throw(_("You are not authorized to view the students of this batch."))
|
||||
|
||||
students = []
|
||||
students_list = frappe.get_all(
|
||||
"LMS Batch Enrollment",
|
||||
@@ -1478,6 +1494,8 @@ def get_quiz_pass_stats(batch):
|
||||
@frappe.whitelist()
|
||||
def get_batch_chart_data(batch):
|
||||
"""Get completion counts per course and assessment"""
|
||||
if not can_modify_batch(batch):
|
||||
frappe.throw(_("You are not authorized to view the chart data of this batch."))
|
||||
if not frappe.db.exists("LMS Batch", batch):
|
||||
frappe.throw(_("The specified batch does not exist."))
|
||||
|
||||
@@ -1614,8 +1632,27 @@ def has_submitted_assessment(assessment, assessment_type, member=None):
|
||||
)
|
||||
|
||||
|
||||
def can_access_topic(doctype, docname):
|
||||
is_student = False
|
||||
if doctype == "Course Lesson":
|
||||
course = frappe.db.get_value("Course Lesson", docname, "course")
|
||||
is_student = frappe.db.exists("LMS Enrollment", {"course": course, "member": frappe.session.user})
|
||||
if not is_student and not can_modify_course(course):
|
||||
return False
|
||||
elif doctype == "LMS Batch":
|
||||
is_student = frappe.db.exists(
|
||||
"LMS Batch Enrollment", {"batch": docname, "member": frappe.session.user}
|
||||
)
|
||||
if not is_student and not can_modify_batch(docname):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_discussion_topics(doctype, docname, single_thread):
|
||||
if not can_access_topic(doctype, docname):
|
||||
frappe.throw(_("You are not authorized to view the discussion topics for this item."))
|
||||
|
||||
if single_thread:
|
||||
filters = {
|
||||
"reference_doctype": doctype,
|
||||
@@ -1657,6 +1694,10 @@ def create_discussion_topic(doctype, docname):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_discussion_replies(topic):
|
||||
doctype = frappe.db.get_value("Discussion Topic", topic, "reference_doctype")
|
||||
if not can_access_topic(doctype, topic):
|
||||
frappe.throw(_("You are not authorized to view the discussion replies for this topic."))
|
||||
|
||||
replies = frappe.get_all(
|
||||
"Discussion Reply",
|
||||
{
|
||||
@@ -1809,6 +1850,7 @@ def calculate_discount_amount(base_amount, coupon):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_lesson_creation_details(course, chapter, lesson):
|
||||
frappe.only_for(["Moderator", "Course Creator"])
|
||||
chapter_name = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": chapter}, "chapter")
|
||||
lesson_name = frappe.db.get_value("Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson")
|
||||
|
||||
@@ -1993,6 +2035,9 @@ def update_certificate_purchase(course, payment_name):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_programs():
|
||||
if not guest_access_allowed():
|
||||
frappe.throw(_("Please login to view programs."))
|
||||
|
||||
enrolled_programs = frappe.get_all(
|
||||
"LMS Program Member", {"member": frappe.session.user}, ["parent as name", "progress"]
|
||||
)
|
||||
@@ -2025,6 +2070,9 @@ def get_programs():
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_program_details(program_name):
|
||||
if not guest_access_allowed():
|
||||
frappe.throw(_("Please login to view program details."))
|
||||
|
||||
program = frappe.db.get_value(
|
||||
"LMS Program",
|
||||
program_name,
|
||||
@@ -2082,9 +2130,6 @@ def enroll_in_program(program):
|
||||
|
||||
|
||||
def validate_program_enrollment(program):
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.throw(_("Please login to enroll in the program."))
|
||||
|
||||
published = frappe.db.get_value("LMS Program", program, "published")
|
||||
if not published:
|
||||
frappe.throw(_("You cannot enroll in an unpublished program."))
|
||||
@@ -2093,6 +2138,9 @@ def validate_program_enrollment(program):
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_batches(filters=None, start=0, order_by="start_date"):
|
||||
if not guest_access_allowed():
|
||||
return []
|
||||
|
||||
if not filters:
|
||||
filters = {}
|
||||
|
||||
@@ -2207,6 +2255,9 @@ def get_palette(full_name):
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
@rate_limit(limit=500, seconds=60 * 60)
|
||||
def get_related_courses(course):
|
||||
if not guest_access_allowed():
|
||||
return []
|
||||
|
||||
related_course_details = []
|
||||
related_courses = frappe.get_all("Related Courses", {"parent": course}, order_by="idx", pluck="course")
|
||||
|
||||
@@ -2262,3 +2313,27 @@ def validate_batch_access(batch):
|
||||
)
|
||||
if not enrollment_exists:
|
||||
frappe.throw(_("You do not have access to this batch."))
|
||||
|
||||
|
||||
def can_modify_course(course):
|
||||
is_instructor = frappe.db.exists(
|
||||
"Course Instructor",
|
||||
{"instructor": frappe.session.user, "parent": course, "parenttype": "LMS Course"},
|
||||
)
|
||||
if not (has_moderator_role() or is_instructor):
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def can_modify_batch(batch):
|
||||
is_instructor = frappe.db.exists(
|
||||
"Course Instructor",
|
||||
{
|
||||
"instructor": frappe.session.user,
|
||||
"parent": batch,
|
||||
"parenttype": "LMS Batch",
|
||||
},
|
||||
)
|
||||
if not (has_moderator_role() or is_instructor):
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
<div class="course-image {% if not course.image %} default-image {% endif %}"
|
||||
{% if course.image %} style="background-image: url( {{ course.image | urlencode }} );" {% endif %}>
|
||||
<div class="course-tags">
|
||||
{% for tag in get_tags(course.name) %}
|
||||
{% for tag in frappe.db.get_value("LMS Course", course.name, "tags").split(",") %}
|
||||
<div class="course-card-pills">{{ tag }}</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user