fix: api permissions

This commit is contained in:
Jannat Patel
2026-01-29 19:52:56 +05:30
parent 3170c066dc
commit 933bc58264
14 changed files with 238 additions and 282 deletions

View File

@@ -26,8 +26,8 @@
</div> </div>
<div class="flex flex-col overflow-y-auto"> <div class="flex flex-col overflow-y-auto">
<div class="p-5"> <div class="p-5 space-y-5">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between">
<div class="font-semibold text-ink-gray-9"> <div class="font-semibold text-ink-gray-9">
{{ __('Submission') }} {{ __('Submission') }}
</div> </div>
@@ -53,7 +53,7 @@
!['Pass', 'Fail'].includes(submissionResource.doc?.status) && !['Pass', 'Fail'].includes(submissionResource.doc?.status) &&
submissionResource.doc?.owner == user.data?.name 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.") }} {{ __("You've successfully submitted the assignment.") }}
{{ {{
@@ -63,12 +63,17 @@
}} }}
{{ __('Feel free to make edits to your submission if needed.') }} {{ __('Feel free to make edits to your submission if needed.') }}
</div> </div>
<div v-if="showUploader()"> <div v-if="showUploader()" class="border rounded-lg p-3">
<div class="text-xs text-ink-gray-5 mt-1 mb-2"> <div class="font-semibold mb-2">
{{ __('Add your assignment as {0}').format(assignment.data.type) }} {{ __('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> </div>
<FileUploader <FileUploader
v-if="!submissionFile" v-if="!submissionResource.doc?.assignment_attachment"
:fileTypes="getType()" :fileTypes="getType()"
:uploadArgs="{ :uploadArgs="{
private: true, private: true,
@@ -87,21 +92,24 @@
</template> </template>
</FileUploader> </FileUploader>
<div v-else> <div v-else>
<div class="flex text-ink-gray-7"> <div class="flex items-center text-ink-gray-7">
<div class="border self-start rounded-md p-2 mr-2">
<FileText class="h-5 w-5 stroke-1.5" />
</div>
<a <a
:href="submissionFile.file_url" :href="submissionResource.doc.assignment_attachment"
target="_blank" 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"> <div class="flex items-center">
{{ submissionFile.file_name }} <div class="border rounded-md p-2 mr-2">
</span> <FileText class="h-5 w-5 stroke-1.5" />
<span class="text-sm text-ink-gray-5 mt-1"> </div>
{{ getFileSize(submissionFile.file_size) }} <span>
</span> {{
submissionResource.doc.assignment_attachment
.split('/')
.pop()
}}
</span>
</div>
</a> </a>
<X <X
v-if="canModifyAssignment" v-if="canModifyAssignment"
@@ -142,13 +150,13 @@
user.data?.name == submissionResource.doc?.owner && user.data?.name == submissionResource.doc?.owner &&
submissionResource.doc?.comments 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"> <div class="text-ink-gray-5 mb-4">
{{ __('Comments by Evaluator') }}: {{ __('Comments by Evaluator') }}
</div> </div>
<div <div
class="leading-5 text-ink-gray-9" class="leading-6 text-ink-gray-9"
v-html="submissionResource.doc.comments" v-html="submissionResource.doc.comments"
></div> ></div>
</div> </div>
@@ -204,10 +212,8 @@ import {
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue' import { computed, inject, onMounted, onBeforeUnmount, ref, watch } from 'vue'
import { FileText, X } from 'lucide-vue-next' import { FileText, X } from 'lucide-vue-next'
import { getFileSize } from '@/utils'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const submissionFile = ref(null)
const answer = ref(null) const answer = ref(null)
const comments = ref(null) const comments = ref(null)
const router = useRouter() const router = useRouter()
@@ -266,9 +272,7 @@ const newSubmission = createResource({
assignment: props.assignmentID, assignment: props.assignmentID,
member: user.data?.name, member: user.data?.name,
} }
if (showUploader()) { if (!showUploader()) {
doc.assignment_attachment = submissionFile.value.file_url
} else {
doc.answer = answer.value doc.answer = answer.value
} }
return { 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({ const submissionResource = createDocumentResource({
doctype: 'LMS Assignment Submission', doctype: 'LMS Assignment Submission',
name: props.submissionName, name: props.submissionName,
@@ -302,11 +293,6 @@ const submissionResource = createDocumentResource({
watch(submissionResource, () => { watch(submissionResource, () => {
if (submissionResource.doc) { if (submissionResource.doc) {
if (submissionResource.doc.assignment_attachment) {
imageResource.reload({
image: submissionResource.doc.assignment_attachment,
})
}
if (submissionResource.doc.answer) { if (submissionResource.doc.answer) {
answer.value = submissionResource.doc.answer answer.value = submissionResource.doc.answer
} }
@@ -315,7 +301,10 @@ watch(submissionResource, () => {
} }
if (submissionResource.isDirty) { if (submissionResource.isDirty) {
isDirty.value = true isDirty.value = true
} else if (showUploader() && !submissionFile.value) { } else if (
showUploader() &&
!submissionResource.doc.assignment_attachment
) {
isDirty.value = true isDirty.value = true
} else if (!showUploader() && !answer.value) { } else if (!showUploader() && !answer.value) {
isDirty.value = true isDirty.value = true
@@ -325,11 +314,17 @@ watch(submissionResource, () => {
} }
}) })
watch(submissionFile, () => { watch(
if (props.submissionName == 'new' && submissionFile.value) { () => submissionResource.doc,
isDirty.value = true () => {
if (
props.submissionName == 'new' &&
submissionResource.doc?.assignment_attachment
) {
isDirty.value = true
}
} }
}) )
const submitAssignment = () => { const submitAssignment = () => {
if (props.submissionName != 'new') { if (props.submissionName != 'new') {
@@ -341,13 +336,13 @@ const submitAssignment = () => {
submissionResource.setValue.submit( submissionResource.setValue.submit(
{ {
...submissionResource.doc, ...submissionResource.doc,
assignment_attachment: submissionFile.value?.file_url,
evaluator: evaluator, evaluator: evaluator,
comments: comments.value, comments: comments.value,
answer: answer.value, answer: answer.value,
}, },
{ {
onSuccess(data) { onSuccess(data) {
isDirty.value = false
toast.success(__('Changes saved successfully')) toast.success(__('Changes saved successfully'))
}, },
} }
@@ -388,7 +383,7 @@ const addNewSubmission = () => {
const saveSubmission = (file) => { const saveSubmission = (file) => {
isDirty.value = true isDirty.value = true
submissionFile.value = file submissionResource.doc.assignment_attachment = file.file_url
} }
const markLessonProgress = () => { const markLessonProgress = () => {
@@ -439,7 +434,7 @@ const validateFile = (file) => {
const removeSubmission = () => { const removeSubmission = () => {
isDirty.value = true isDirty.value = true
submissionFile.value = null submissionResource.doc.assignment_attachment = ''
} }
const canGradeSubmission = computed(() => { const canGradeSubmission = computed(() => {

View File

@@ -1,7 +1,7 @@
<template> <template>
<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 && batch.data.seats_left > 0"
class="text-sm bg-green-100 text-green-700 px-2 py-1 rounded-md" class="text-sm bg-green-100 text-green-700 px-2 py-1 rounded-md"
:class=" :class="
batch.data.amount || batch.data.courses.length batch.data.amount || batch.data.courses.length
@@ -9,16 +9,16 @@
: 'w-fit mb-4' : 'w-fit mb-4'
" "
> >
{{ seats_left }} {{ batch.data.seats_left }}
<span v-if="seats_left > 1"> <span v-if="batch.data.seats_left > 1">
{{ __('Seats Left') }} {{ __('Seats Left') }}
</span> </span>
<span v-else-if="seats_left == 1"> <span v-else-if="batch.data.seats_left == 1">
{{ __('Seat Left') }} {{ __('Seat Left') }}
</span> </span>
</div> </div>
<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" class="text-xs bg-red-100 text-red-700 float-right px-2 py-0.5 rounded-md"
> >
{{ __('Sold Out') }} {{ __('Sold Out') }}
@@ -54,6 +54,7 @@
{{ batch.data.timezone }} {{ batch.data.timezone }}
</span> </span>
</div> </div>
<div v-if="!readOnlyMode"> <div v-if="!readOnlyMode">
<router-link <router-link
v-if="canAccessBatch" 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(() => { 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(() => { const isModerator = computed(() => {
@@ -218,6 +214,9 @@ const isInstructor = computed(() => {
}) })
const canAccessBatch = computed(() => { const canAccessBatch = computed(() => {
if (!user.data) {
return false
}
return isModerator.value || isStudent.value || isEvaluator.value return isModerator.value || isStudent.value || isEvaluator.value
}) })

View File

@@ -34,7 +34,12 @@
<img <img
v-if="type == 'image'" v-if="type == 'image'"
:src="modelValue" :src="modelValue"
class="border rounded-md w-44 h-auto" :class="[
'border object-cover',
shape === 'circle'
? 'w-20 h-20 rounded-full'
: 'w-44 h-auto rounded-md',
]"
/> />
<video v-else controls class="border rounded-md w-44 h-auto"> <video v-else controls class="border rounded-md w-44 h-auto">
<source :src="modelValue" /> <source :src="modelValue" />
@@ -72,6 +77,7 @@ const props = withDefaults(
description?: string description?: string
type?: 'image' | 'video' type?: 'image' | 'video'
required?: boolean required?: boolean
shape?: 'square' | 'circle'
}>(), }>(),
{ {
modelValue: '', modelValue: '',
@@ -79,6 +85,7 @@ const props = withDefaults(
description: '', description: '',
type: 'image', type: 'image',
required: true, required: true,
shape: 'square',
} }
) )

View File

@@ -1,17 +1,25 @@
<template> <template>
<Dialog <Dialog
v-model="show"
:options="{ :options="{
size: '3xl', size: '3xl',
}" }"
> >
<template #body-header> <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"> <div class="text-2xl font-semibold leading-6 text-ink-gray-9">
{{ __('Edit Profile') }} {{ __('Edit Profile') }}
</div> </div>
<Badge v-if="isDirty" class="ml-4" theme="orange"> <div class="space-x-2">
{{ __('Not Saved') }} <Badge v-if="isDirty" theme="orange">
</Badge> {{ __('Not Saved') }}
</Badge>
<div class="pb-5 float-right">
<Button variant="solid" @click="saveProfile()">
{{ __('Save') }}
</Button>
</div>
</div>
</div> </div>
</template> </template>
<template #body-content> <template #body-content>
@@ -19,52 +27,13 @@
<div class="grid grid-cols-2 gap-10"> <div class="grid grid-cols-2 gap-10">
<div class="space-y-4"> <div class="space-y-4">
<div class="space-y-4"> <div class="space-y-4">
<div> <Uploader
<div class="text-xs text-ink-gray-5 mb-1"> v-model="profile.image"
{{ __('Profile Image') }} :label="__('Profile Image')"
</div> :required="true"
<FileUploader shape="circle"
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"
/>
<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 <FormControl
v-model="profile.first_name" v-model="profile.first_name"
:label="__('First Name')" :label="__('First Name')"
@@ -115,13 +84,6 @@
</div> </div>
</div> </div>
</template> </template>
<template #actions="{ close }">
<div class="pb-5 float-right">
<Button variant="solid" @click="saveProfile(close)">
{{ __('Save') }}
</Button>
</div>
</template>
</Dialog> </Dialog>
</template> </template>
<script setup> <script setup>
@@ -131,15 +93,14 @@ import {
createResource, createResource,
Dialog, Dialog,
FormControl, FormControl,
FileUploader,
TextEditor, TextEditor,
toast, toast,
} from 'frappe-ui' } from 'frappe-ui'
import { ref, reactive, watch } from 'vue' import { ref, reactive, watch } from 'vue'
import { X } from 'lucide-vue-next' import { sanitizeHTML } from '@/utils'
import { getFileSize, sanitizeHTML } from '@/utils'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
const show = defineModel()
const reloadProfile = defineModel('reloadProfile') const reloadProfile = defineModel('reloadProfile')
const hasLanguageChanged = ref(false) const hasLanguageChanged = ref(false)
const isDirty = ref(false) const isDirty = ref(false)
@@ -163,19 +124,6 @@ const profile = reactive({
twitter: '', 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({ const updateProfile = createResource({
url: 'frappe.client.set_value', url: 'frappe.client.set_value',
makeParams(values) { makeParams(values) {
@@ -183,7 +131,7 @@ const updateProfile = createResource({
doctype: 'User', doctype: 'User',
name: props.profile.data.name, name: props.profile.data.name,
fieldname: { fieldname: {
user_image: profile.image?.file_url || null, user_image: profile.image || null,
...profile, ...profile,
}, },
} }
@@ -193,13 +141,13 @@ const updateProfile = createResource({
}, },
}) })
const saveProfile = (close) => { const saveProfile = () => {
profile.bio = sanitizeHTML(profile.bio) profile.bio = sanitizeHTML(profile.bio)
updateProfile.submit( updateProfile.submit(
{}, {},
{ {
onSuccess() { onSuccess() {
close() show.value = false
reloadProfile.value.reload() reloadProfile.value.reload()
if (hasLanguageChanged.value) { if (hasLanguageChanged.value) {
hasLanguageChanged.value = false 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( watch(
() => profile, () => profile,
(newVal) => { (newVal) => {
@@ -240,7 +173,7 @@ watch(
return return
} }
} }
if (profile.image?.file_url !== props.profile.data.user_image) { if (profile.image !== props.profile.data.user_image) {
isDirty.value = true isDirty.value = true
return return
} }
@@ -262,7 +195,7 @@ watch(
profile.linkedin = newVal.linkedin profile.linkedin = newVal.linkedin
profile.github = newVal.github profile.github = newVal.github
profile.twitter = newVal.twitter profile.twitter = newVal.twitter
if (newVal.user_image) imageResource.submit({ image: newVal.user_image }) profile.image = newVal.user_image
isDirty.value = false isDirty.value = false
} }
} }

View File

@@ -186,8 +186,9 @@ const openProfile = (username: string) => {
} }
const deleteEvaluator = (evaluator: string) => { const deleteEvaluator = (evaluator: string) => {
call('lms.lms.api.delete_evaluator', { call('frappe.client.delete', {
evaluator: evaluator, doctype: 'Course Evaluator',
name: evaluator,
}) })
.then(() => { .then(() => {
toast.success(__('Evaluator deleted successfully')) toast.success(__('Evaluator deleted successfully'))

View File

@@ -269,12 +269,13 @@ const iconProps = {
onMounted(() => { onMounted(() => {
setUpOnboarding() setUpOnboarding()
addKeyboardShortcut() addKeyboardShortcut()
updateSidebarLinks()
socket.on('publish_lms_notifications', (data) => { socket.on('publish_lms_notifications', (data) => {
unreadNotifications.reload() unreadNotifications.reload()
}) })
}) })
const setSidebarLinks = () => { const updateSidebarLinksVisibility = () => {
sidebarSettings.reload( sidebarSettings.reload(
{}, {},
{ {
@@ -591,10 +592,18 @@ watch(userResource, async () => {
await programs.reload() await programs.reload()
setUpOnboarding() setUpOnboarding()
} }
sidebarLinks.value = getSidebarLinks() updateSidebarLinks()
setSidebarLinks()
}) })
watch(settingsStore.settings, () => {
updateSidebarLinks()
})
const updateSidebarLinks = () => {
sidebarLinks.value = getSidebarLinks()
updateSidebarLinksVisibility()
}
const redirectToWebsite = () => { const redirectToWebsite = () => {
window.open('https://frappe.io/learning', '_blank') window.open('https://frappe.io/learning', '_blank')
} }

View File

@@ -134,6 +134,7 @@ import {
import { computed, inject, onMounted, ref } from 'vue' import { computed, inject, onMounted, ref } from 'vue'
import { GraduationCap } from 'lucide-vue-next' import { GraduationCap } from 'lucide-vue-next'
import { sessionStore } from '../stores/session' import { sessionStore } from '../stores/session'
import { useRouter } from 'vue-router'
import EmptyState from '@/components/EmptyState.vue' import EmptyState from '@/components/EmptyState.vue'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
@@ -145,8 +146,14 @@ const hiring = ref(false)
const { brand } = sessionStore() const { brand } = sessionStore()
const memberCount = ref(0) const memberCount = ref(0)
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
const user = inject('$user')
const router = useRouter()
onMounted(() => { onMounted(() => {
if (!user.data) {
router.push({ name: 'Courses' })
return
}
setFiltersFromQuery() setFiltersFromQuery()
updateParticipants() updateParticipants()
}) })
@@ -171,7 +178,7 @@ const categories = createListResource({
doctype: 'LMS Certificate', doctype: 'LMS Certificate',
url: 'lms.lms.api.get_certification_categories', url: 'lms.lms.api.get_certification_categories',
cache: ['certification_categories'], cache: ['certification_categories'],
auto: true, auto: user.data ? true : false,
transform(data) { transform(data) {
data.unshift({ label: __(' '), value: ' ' }) data.unshift({ label: __(' '), value: ' ' })
return data return data

View File

@@ -326,6 +326,7 @@
@updateNotes="updateNotes" @updateNotes="updateNotes"
/> />
<VideoStatistics <VideoStatistics
v-if="showStatsDialog"
v-model="showStatsDialog" v-model="showStatsDialog"
:lessonName="lesson.data?.name" :lessonName="lesson.data?.name"
:lessonTitle="lesson.data?.title" :lessonTitle="lesson.data?.title"
@@ -870,6 +871,7 @@ const scrollDiscussionsIntoView = () => {
} }
const updateNotes = () => { const updateNotes = () => {
if (!user.data) return
notes.update({ notes.update({
filters: { filters: {
lesson: lesson.data?.name, lesson: lesson.data?.name,

View File

@@ -185,10 +185,9 @@ const unReadNotifications = createListResource({
doctype: 'Notification Log', doctype: 'Notification Log',
url: 'lms.lms.api.get_notifications', url: 'lms.lms.api.get_notifications',
filters: { filters: {
for_user: user.data?.name,
read: 0, read: 0,
}, },
auto: true, auto: user.data ? true : false,
cache: 'Unread Notifications', cache: 'Unread Notifications',
}) })
@@ -196,18 +195,17 @@ const readNotifications = createListResource({
doctype: 'Notification Log', doctype: 'Notification Log',
url: 'lms.lms.api.get_notifications', url: 'lms.lms.api.get_notifications',
filters: { filters: {
for_user: user.data?.name,
read: 1, read: 1,
}, },
auto: true, auto: user.data ? true : false,
cache: 'Read Notifications', cache: 'Read Notifications',
}) })
const markAsRead = createResource({ const markAsRead = createResource({
url: 'lms.lms.api.mark_as_read', url: 'frappe.desk.doctype.notification_log.notification_log.mark_as_read',
makeParams(values) { makeParams(values) {
return { return {
name: values.name, docname: values.name,
} }
}, },
onSuccess(data) { onSuccess(data) {
@@ -217,7 +215,7 @@ const markAsRead = createResource({
}) })
const markAllAsRead = 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) { onSuccess(data) {
unReadNotifications.reload() unReadNotifications.reload()
readNotifications.reload() readNotifications.reload()

View File

@@ -13,6 +13,8 @@ export const sessionStore = defineStore('lms-session', () => {
let _sessionUser = cookies.get('user_id') let _sessionUser = cookies.get('user_id')
if (_sessionUser === 'Guest') { if (_sessionUser === 'Guest') {
_sessionUser = null _sessionUser = null
} else {
userResource.reload()
} }
return _sessionUser return _sessionUser
} }

View File

@@ -9,7 +9,6 @@ export const usersStore = defineStore('lms-users', () => {
window.location.href = '/login' window.location.href = '/login'
} }
}, },
auto: true,
}) })
const allUsers = createResource({ const allUsers = createResource({

View File

@@ -490,6 +490,9 @@ const getSidebarItems = () => {
icon: 'GraduationCap', icon: 'GraduationCap',
to: 'CertifiedParticipants', to: 'CertifiedParticipants',
activeFor: ['CertifiedParticipants'], activeFor: ['CertifiedParticipants'],
condition: () => {
return userResource?.data
},
}, },
{ {
label: 'Jobs', label: 'Jobs',

View File

@@ -35,10 +35,14 @@ from lms.lms.utils import (
get_course_details, get_course_details,
get_instructors, get_instructors,
get_lesson_count, get_lesson_count,
has_course_instructor_role,
has_evaluator_role,
has_moderator_role,
has_student_role,
) )
@frappe.whitelist(allow_guest=True) @frappe.whitelist()
def get_user_info(): def get_user_info():
if frappe.session.user == "Guest": if frappe.session.user == "Guest":
return None return None
@@ -222,7 +226,6 @@ def get_chart_details():
return details return details
@frappe.whitelist()
def get_file_info(file_url): def get_file_info(file_url):
"""Get file info for the given file URL.""" """Get file info for the given file URL."""
file_info = frappe.db.get_value( file_info = frappe.db.get_value(
@@ -234,17 +237,20 @@ def get_file_info(file_url):
@frappe.whitelist(allow_guest=True) @frappe.whitelist(allow_guest=True)
def get_branding(): def get_branding():
"""Get branding details.""" """Get branding details."""
website_settings = frappe.get_single("Website Settings") fields = ["app_name"]
image_fields = ["banner_image", "footer_logo", "favicon"] image_fields = ["banner_image", "footer_logo", "favicon", "app_logo"]
fields = fields + image_fields
settings = frappe._dict()
for field in image_fields: for field in fields:
if website_settings.get(field): value = frappe.get_cached_value("Website Settings", None, field)
file_info = get_file_info(website_settings.get(field)) if field in image_fields and value:
website_settings.update({field: json.loads(json.dumps(file_info))}) file_info = get_file_info(value)
settings.update({field: json.loads(json.dumps(file_info))})
else: else:
website_settings.update({field: None}) settings.update({field: value})
return website_settings return settings
@frappe.whitelist() @frappe.whitelist()
@@ -284,7 +290,7 @@ def get_evaluator_details(evaluator):
} }
@frappe.whitelist(allow_guest=True) @frappe.whitelist()
def get_certified_participants(filters=None, start=0, page_length=100): def get_certified_participants(filters=None, start=0, page_length=100):
query = get_certification_query(filters) query = get_certification_query(filters)
query = query.orderby("issue_date", order=frappe.qb.desc).offset(start).limit(page_length) query = query.orderby("issue_date", order=frappe.qb.desc).offset(start).limit(page_length)
@@ -338,14 +344,14 @@ def get_certification_query(filters):
return query return query
@frappe.whitelist(allow_guest=True) @frappe.whitelist()
def get_count_of_certified_members(filters=None): def get_count_of_certified_members(filters=None):
query = get_certification_query(filters) query = get_certification_query(filters)
result = query.run(as_dict=True) result = query.run(as_dict=True)
return len(result) or 0 return len(result) or 0
@frappe.whitelist(allow_guest=True) @frappe.whitelist()
def get_certification_categories(): def get_certification_categories():
categories = [] categories = []
seen = set() seen = set()
@@ -367,20 +373,6 @@ def get_certification_categories():
return 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() @frappe.whitelist()
def get_all_users(): def get_all_users():
frappe.only_for(["Moderator", "Course Creator", "Batch Evaluator"]) frappe.only_for(["Moderator", "Course Creator", "Batch Evaluator"])
@@ -395,28 +387,13 @@ def get_all_users():
return {user.name: user for user in 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) @frappe.whitelist(allow_guest=True)
def get_sidebar_settings(): def get_sidebar_settings():
lms_settings = frappe.get_single("LMS 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 = [ items = [
"courses", "courses",
"batches", "batches",
@@ -445,6 +422,7 @@ def get_sidebar_settings():
@frappe.whitelist() @frappe.whitelist()
def update_sidebar_item(webpage, icon): def update_sidebar_item(webpage, icon):
frappe.only_for("Moderator")
filters = { filters = {
"web_page": webpage, "web_page": webpage,
"parenttype": "LMS Settings", "parenttype": "LMS Settings",
@@ -463,6 +441,7 @@ def update_sidebar_item(webpage, icon):
@frappe.whitelist() @frappe.whitelist()
def delete_sidebar_item(webpage): def delete_sidebar_item(webpage):
frappe.only_for("Moderator")
return frappe.db.delete( return frappe.db.delete(
"LMS Sidebar Item", "LMS Sidebar Item",
{ {
@@ -476,6 +455,10 @@ def delete_sidebar_item(webpage):
@frappe.whitelist() @frappe.whitelist()
def delete_lesson(lesson, chapter): 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 # Delete Reference
chapter = frappe.get_doc("Course Chapter", chapter) chapter = frappe.get_doc("Course Chapter", chapter)
chapter.lessons = [row for row in chapter.lessons if row.lesson != lesson] chapter.lessons = [row for row in chapter.lessons if row.lesson != lesson]
@@ -490,8 +473,11 @@ def delete_lesson(lesson, chapter):
@frappe.whitelist() @frappe.whitelist()
def update_lesson_index(lesson, sourceChapter, targetChapter, idx): 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) update_source_chapter(lesson, sourceChapter, idx, hasMoved)
if not hasMoved: if not hasMoved:
update_target_chapter(lesson, targetChapter, idx) update_target_chapter(lesson, targetChapter, idx)
@@ -550,6 +536,10 @@ def update_index(lessons, chapter):
@frappe.whitelist() @frappe.whitelist()
def update_chapter_index(chapter, course, idx): def update_chapter_index(chapter, course, idx):
"""Update the index of a chapter within a course""" """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( chapters = frappe.get_all(
"Chapter Reference", "Chapter Reference",
{"parent": course}, {"parent": course},
@@ -566,26 +556,9 @@ def update_chapter_index(chapter, course, idx):
frappe.db.set_value("Chapter Reference", {"chapter": chapter_name, "parent": course}, "idx", i + 1) 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() @frappe.whitelist()
def get_members(start=0, search=""): def get_members(start=0, search=""):
frappe.only_for(["Moderator"])
filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]} filters = {"enabled": 1, "name": ["not in", ["Administrator", "Guest"]]}
or_filters = {} or_filters = {}
@@ -652,6 +625,7 @@ def save_evaluation_details(
""" """
Save evaluation details for a member against a course. 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}) evaluation = frappe.db.exists("LMS Certificate Evaluation", {"member": member, "course": course})
details = { details = {
@@ -695,6 +669,7 @@ def save_certificate_details(
""" """
Save certificate details for a member against a course. 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}) certificate = frappe.db.exists("LMS Certificate", {"member": member, "course": course})
details = { details = {
@@ -729,16 +704,9 @@ def delete_documents(doctype, documents):
frappe.delete_doc(doctype, doc) frappe.delete_doc(doctype, doc)
@frappe.whitelist(allow_guest=True)
def get_count(doctype, filters):
return frappe.db.count(
doctype,
filters=filters,
)
@frappe.whitelist() @frappe.whitelist()
def get_payment_gateway_details(payment_gateway): def get_payment_gateway_details(payment_gateway):
frappe.only_for("Moderator")
gateway = frappe.get_doc("Payment Gateway", payment_gateway) gateway = frappe.get_doc("Payment Gateway", payment_gateway)
if gateway.gateway_controller is None: if gateway.gateway_controller is None:
@@ -794,6 +762,7 @@ def get_transformed_fields(meta, data=None):
@frappe.whitelist() @frappe.whitelist()
def get_new_gateway_fields(doctype): def get_new_gateway_fields(doctype):
frappe.only_for("Moderator")
try: try:
meta = frappe.get_meta(doctype).fields meta = frappe.get_meta(doctype).fields
except Exception: except Exception:
@@ -824,6 +793,18 @@ def update_course_statistics():
@frappe.whitelist() @frappe.whitelist()
def get_announcements(batch): 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( communications = frappe.get_all(
"Communication", "Communication",
filters={ filters={
@@ -848,8 +829,21 @@ def get_announcements(batch):
return communications return communications
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
@frappe.whitelist() @frappe.whitelist()
def delete_course(course): 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 Enrollment", {"course": course})
frappe.db.delete("LMS Course Progress", {"course": course}) frappe.db.delete("LMS Course Progress", {"course": course})
frappe.db.set_value("LMS Quiz", {"course": course}, "course", None) frappe.db.set_value("LMS Quiz", {"course": course}, "course", None)
@@ -882,8 +876,25 @@ def delete_course(course):
frappe.delete_doc("LMS Course", course) frappe.delete_doc("LMS Course", course)
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
@frappe.whitelist() @frappe.whitelist()
def delete_batch(batch): 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("LMS Batch Enrollment", {"batch": batch})
frappe.db.delete("Batch Course", {"parent": batch, "parenttype": "LMS Batch"}) frappe.db.delete("Batch Course", {"parent": batch, "parenttype": "LMS Batch"})
frappe.db.delete("LMS Assessment", {"parent": batch, "parenttype": "LMS Batch"}) frappe.db.delete("LMS Assessment", {"parent": batch, "parenttype": "LMS Batch"})
@@ -927,6 +938,9 @@ def give_discussions_permission():
@frappe.whitelist() @frappe.whitelist()
def upsert_chapter(title, course, is_scorm_package, scorm_package, name=None): 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}) values = frappe._dict({"title": title, "course": course, "is_scorm_package": is_scorm_package})
if is_scorm_package: if is_scorm_package:
@@ -1049,6 +1063,10 @@ def add_lesson(title, chapter, course, idx):
@frappe.whitelist() @frappe.whitelist()
def delete_chapter(chapter): 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( chapterInfo = frappe.db.get_value(
"Course Chapter", chapter, ["is_scorm_package", "scorm_package_path"], as_dict=True "Course Chapter", chapter, ["is_scorm_package", "scorm_package_path"], as_dict=True
) )
@@ -1091,9 +1109,9 @@ def mark_lesson_progress(course, chapter_number, lesson_number):
@frappe.whitelist() @frappe.whitelist()
def get_heatmap_data(member=None, base_days=200): def get_heatmap_data(member, base_days=200):
if not member: if not (has_course_instructor_role() or has_moderator_role() or has_evaluator_role()):
member = frappe.session.user 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) base_date, start_date, number_of_days, days = calculate_date_ranges(base_days)
date_count = initialize_date_count(days) date_count = initialize_date_count(days)
@@ -1206,6 +1224,8 @@ def get_week_difference(start_date, current_date):
@frappe.whitelist() @frappe.whitelist()
def get_notifications(filters): def get_notifications(filters):
filters = frappe._dict(filters or {})
filters.for_user = frappe.session.user
notifications = frappe.get_all( notifications = frappe.get_all(
"Notification Log", "Notification Log",
filters, filters,
@@ -1305,9 +1325,8 @@ def get_lms_settings():
@frappe.whitelist() @frappe.whitelist()
def cancel_evaluation(evaluation): def cancel_evaluation(evaluation):
evaluation = frappe._dict(evaluation) evaluation = frappe._dict(evaluation)
if evaluation.member != frappe.session.user: 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") frappe.db.set_value("LMS Certificate Request", evaluation.name, "status", "Cancelled")
events = frappe.get_all( events = frappe.get_all(
@@ -1405,16 +1424,6 @@ def add_an_evaluator(email):
return evaluator 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() @frappe.whitelist()
def capture_user_persona(responses): def capture_user_persona(responses):
frappe.only_for("System Manager") frappe.only_for("System Manager")
@@ -1447,6 +1456,7 @@ def get_meta_info(type, route):
@frappe.whitelist() @frappe.whitelist()
def update_meta_info(meta_type, route, meta_tags): 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_data_permissions(meta_type)
validate_meta_tags(meta_tags) validate_meta_tags(meta_tags)
@@ -1547,6 +1557,10 @@ def make_new_exercise_submission(exercise, code, test_cases):
def update_exercise_submission(submission, 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) update_test_cases(test_cases, submission)
status = get_exercise_status(test_cases) status = get_exercise_status(test_cases)
frappe.db.set_value("LMS Programming Exercise Submission", submission, {"status": status, "code": code}) frappe.db.set_value("LMS Programming Exercise Submission", submission, {"status": status, "code": code})
@@ -1619,6 +1633,11 @@ def track_new_watch_time(lesson, video):
@frappe.whitelist() @frappe.whitelist()
def get_course_progress_distribution(course): def get_course_progress_distribution(course):
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_all( all_progress = frappe.get_all(
"LMS Enrollment", "LMS Enrollment",
{ {
@@ -1723,9 +1742,6 @@ def get_profile_details(username):
@frappe.whitelist() @frappe.whitelist()
def get_streak_info(): def get_streak_info():
if frappe.session.user == "Guest":
return {}
all_dates = fetch_activity_dates(frappe.session.user) all_dates = fetch_activity_dates(frappe.session.user)
streak, longest_streak = calculate_streaks(all_dates) streak, longest_streak = calculate_streaks(all_dates)
current_streak = calculate_current_streak(all_dates, streak) current_streak = calculate_current_streak(all_dates, streak)
@@ -1794,8 +1810,6 @@ def calculate_current_streak(all_dates, streak):
@frappe.whitelist() @frappe.whitelist()
def get_my_live_classes(): def get_my_live_classes():
my_live_classes = [] my_live_classes = []
if frappe.session.user == "Guest":
return my_live_classes
batches = frappe.get_all( batches = frappe.get_all(
"LMS Batch Enrollment", "LMS Batch Enrollment",
@@ -1840,8 +1854,6 @@ def get_my_live_classes():
@frappe.whitelist() @frappe.whitelist()
def get_created_courses(): def get_created_courses():
created_courses = [] created_courses = []
if frappe.session.user == "Guest":
return created_courses
CourseInstructor = frappe.qb.DocType("Course Instructor") CourseInstructor = frappe.qb.DocType("Course Instructor")
Course = frappe.qb.DocType("LMS Course") Course = frappe.qb.DocType("LMS Course")
@@ -1869,8 +1881,6 @@ def get_created_courses():
@frappe.whitelist() @frappe.whitelist()
def get_created_batches(): def get_created_batches():
created_batches = [] created_batches = []
if frappe.session.user == "Guest":
return created_batches
CourseInstructor = frappe.qb.DocType("Course Instructor") CourseInstructor = frappe.qb.DocType("Course Instructor")
Batch = frappe.qb.DocType("LMS Batch") Batch = frappe.qb.DocType("LMS Batch")
@@ -1898,9 +1908,6 @@ def get_created_batches():
@frappe.whitelist() @frappe.whitelist()
def get_admin_live_classes(): def get_admin_live_classes():
if frappe.session.user == "Guest":
return []
CourseInstructor = frappe.qb.DocType("Course Instructor") CourseInstructor = frappe.qb.DocType("Course Instructor")
LMSLiveClass = frappe.qb.DocType("LMS Live Class") LMSLiveClass = frappe.qb.DocType("LMS Live Class")
@@ -1931,9 +1938,6 @@ def get_admin_live_classes():
@frappe.whitelist() @frappe.whitelist()
def get_admin_evals(): def get_admin_evals():
if frappe.session.user == "Guest":
return []
evals = frappe.get_all( evals = frappe.get_all(
"LMS Certificate Request", "LMS Certificate Request",
{ {
@@ -1963,9 +1967,6 @@ def get_admin_evals():
@frappe.whitelist() @frappe.whitelist()
def get_my_courses(): def get_my_courses():
my_courses = [] my_courses = []
if frappe.session.user == "Guest":
return my_courses
courses = get_my_latest_courses() courses = get_my_latest_courses()
if not len(courses): if not len(courses):
@@ -2017,9 +2018,6 @@ def get_popular_courses():
@frappe.whitelist() @frappe.whitelist()
def get_my_batches(): def get_my_batches():
my_batches = [] my_batches = []
if frappe.session.user == "Guest":
return my_batches
batches = get_my_latest_batches() batches = get_my_latest_batches()
if not len(batches): if not len(batches):
@@ -2060,5 +2058,6 @@ def get_upcoming_batches():
@frappe.whitelist() @frappe.whitelist()
def delete_programming_exercise(exercise): 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 Submission", {"exercise": exercise})
frappe.db.delete("LMS Programming Exercise", exercise) frappe.db.delete("LMS Programming Exercise", exercise)

View File

@@ -1179,6 +1179,8 @@ def get_batch_details(batch):
if batch_details.seat_count: if batch_details.seat_count:
batch_details.seats_left = batch_details.seat_count - len(batch_students) batch_details.seats_left = batch_details.seat_count - len(batch_students)
print(batch_details.seat_count, len(batch_students))
return batch_details return batch_details