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 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(() => {

View File

@@ -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
})

View File

@@ -34,7 +34,12 @@
<img
v-if="type == 'image'"
: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">
<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',
}
)

View File

@@ -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
}
}

View File

@@ -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'))

View File

@@ -269,12 +269,13 @@ const iconProps = {
onMounted(() => {
setUpOnboarding()
addKeyboardShortcut()
updateSidebarLinks()
socket.on('publish_lms_notifications', (data) => {
unreadNotifications.reload()
})
})
const setSidebarLinks = () => {
const updateSidebarLinksVisibility = () => {
sidebarSettings.reload(
{},
{
@@ -591,10 +592,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')
}

View File

@@ -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

View File

@@ -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,

View File

@@ -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()

View File

@@ -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
}

View File

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

View File

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