Merge pull request #2035 from pateljannat/issues-176

fix: api permissions
This commit is contained in:
Jannat Patel
2026-01-29 21:54:32 +05:30
committed by GitHub
25 changed files with 311 additions and 582 deletions
+49 -54
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(() => {
+12 -13
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
})
@@ -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>
+25 -92
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
}
}
@@ -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'))
+12 -3
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(
{},
{
@@ -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')
}
+8 -1
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
+2
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,
+5 -7
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()
+2
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
}
-1
View File
@@ -9,7 +9,6 @@ export const usersStore = defineStore('lms-users', () => {
window.location.href = '/login'
}
},
auto: true,
})
const allUsers = createResource({
+3
View File
@@ -489,6 +489,9 @@ const getSidebarItems = () => {
icon: 'GraduationCap',
to: 'CertifiedParticipants',
activeFor: ['CertifiedParticipants'],
condition: () => {
return userResource?.data
},
},
{
label: 'Jobs',
-1
View File
@@ -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
View File
@@ -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
-6
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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>