fix: permissions cleanup

This commit is contained in:
Jannat Patel
2026-02-02 13:14:16 +05:30
parent ab155ae609
commit 14e669435f
13 changed files with 105 additions and 272 deletions
@@ -137,11 +137,12 @@ import {
} from 'lucide-vue-next'
import { inject, ref, getCurrentInstance, computed } from 'vue'
import { formatTime } from '@/utils'
import { Button, createResource, call } from 'frappe-ui'
import { Button, createResource, createListResource, call } from 'frappe-ui'
import EvaluationModal from '@/components/Modals/EvaluationModal.vue'
import { Menu, MenuButton, MenuItems, MenuItem } from '@headlessui/vue'
const dayjs = inject('$dayjs')
const user = inject('$user')
const showEvalModal = ref(false)
const app = getCurrentInstance()
const { $dialog } = app.appContext.config.globalProperties
@@ -165,12 +166,26 @@ const props = defineProps({
},
})
const upcoming_evals = createResource({
url: 'lms.lms.utils.get_upcoming_evals',
params: {
courses: props.courses.map((course) => course.course),
batch: props.batch,
const upcoming_evals = createListResource({
doctype: 'LMS Certificate Request',
filters: {
course: props.courses?.length
? ['in', props.courses.map((course) => course.course)]
: undefined,
batch_name: props.batch || undefined,
status: 'Upcoming',
member: user?.data?.name,
date: ['>=', dayjs().format('YYYY-MM-DD')],
},
fields: [
'name',
'date',
'start_time',
'evaluator_name',
'course_title',
'google_meet_link',
],
orderBy: 'date',
auto: true,
})
-3
View File
@@ -140,9 +140,6 @@ const assignmentFilter = computed(() => {
if (typeFilter.value) {
filters.type = typeFilter.value
}
if (!user.data?.is_moderator) {
filters.owner = user.data?.email
}
return filters
})
+9 -2
View File
@@ -60,8 +60,15 @@ const currentTab = ref<'student' | 'instructor'>('instructor')
const showStreakModal = ref(false)
onMounted(() => {
call('lms.lms.utils.get_upcoming_evals').then((data: any) => {
evalCount.value = data.length
call('frappe.client.get_count', {
doctype: 'LMS Certificate Request',
filters: {
member: user?.data?.name,
status: 'Upcoming',
date: ['>=', inject<any>('$dayjs')().format('YYYY-MM-DD')],
},
}).then((data: any) => {
evalCount.value = data
})
})
+72 -72
View File
@@ -1,5 +1,77 @@
<template>
<div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-5 mt-10">
<UpcomingEvaluations :forHome="true" />
<div v-if="myLiveClasses.data?.length">
<div class="font-semibold text-lg mb-3 text-ink-gray-9">
{{ __('Upcoming Live Classes') }}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div
v-for="cls in myLiveClasses.data"
class="border rounded-md hover:border-outline-gray-3 p-2"
>
<div class="font-semibold text-ink-gray-9 text-lg leading-5 mb-1">
{{ cls.title }}
</div>
<div class="text-ink-gray-5 leading-5 mb-4">
{{ cls.description }}
</div>
<div class="mt-auto space-y-4 text-ink-gray-7">
<div class="flex items-center space-x-2">
<Calendar class="w-4 h-4 stroke-1.5" />
<span>
{{ dayjs(cls.date).format('DD MMMM YYYY') }}
</span>
</div>
<div class="flex items-center space-x-2">
<Clock class="w-4 h-4 stroke-1.5" />
<span>
{{ formatTime(cls.time) }} -
{{ dayjs(getClassEnd(cls)).format('HH:mm A') }}
</span>
</div>
<div
v-if="canAccessClass(cls)"
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
>
<a
v-if="user.data?.is_moderator || user.data?.is_evaluator"
:href="cls.start_url"
target="_blank"
class="cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
:class="cls.join_url ? 'w-full' : 'w-1/2'"
>
<Monitor class="h-4 w-4 stroke-1.5" />
{{ __('Start') }}
</a>
<a
:href="cls.join_url"
target="_blank"
class="w-full cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
>
<Video class="h-4 w-4 stroke-1.5" />
{{ __('Join') }}
</a>
</div>
<Tooltip
v-else-if="hasClassEnded(cls)"
:text="__('This class has ended')"
placement="right"
>
<div class="flex items-center space-x-2 text-ink-amber-3 w-fit">
<Info class="w-4 h-4 stroke-1.5" />
<span>
{{ __('Ended') }}
</span>
</div>
</Tooltip>
</div>
</div>
</div>
</div>
</div>
<div v-if="myCourses.data?.length" class="mt-10">
<div class="flex items-center justify-between mb-3">
<span class="font-semibold text-lg text-ink-gray-9">
@@ -63,78 +135,6 @@
</router-link>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-10 md:gap-5 mt-10">
<UpcomingEvaluations :forHome="true" />
<div v-if="myLiveClasses.data?.length">
<div class="font-semibold text-lg mb-3 text-ink-gray-9">
{{ __('Upcoming Live Classes') }}
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-5">
<div
v-for="cls in myLiveClasses.data"
class="border rounded-md hover:border-outline-gray-3 p-2"
>
<div class="font-semibold text-ink-gray-9 text-lg leading-5 mb-1">
{{ cls.title }}
</div>
<div class="text-ink-gray-7 text-sm leading-5 mb-4">
{{ cls.description }}
</div>
<div class="mt-auto space-y-3 text-ink-gray-7 text-sm">
<div class="flex items-center space-x-2">
<Calendar class="w-4 h-4 stroke-1.5" />
<span>
{{ dayjs(cls.date).format('DD MMMM YYYY') }}
</span>
</div>
<div class="flex items-center space-x-2">
<Clock class="w-4 h-4 stroke-1.5" />
<span>
{{ formatTime(cls.time) }} -
{{ dayjs(getClassEnd(cls)).format('HH:mm A') }}
</span>
</div>
<div
v-if="canAccessClass(cls)"
class="flex items-center space-x-2 text-ink-gray-9 mt-auto"
>
<a
v-if="user.data?.is_moderator || user.data?.is_evaluator"
:href="cls.start_url"
target="_blank"
class="cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
:class="cls.join_url ? 'w-full' : 'w-1/2'"
>
<Monitor class="h-4 w-4 stroke-1.5" />
{{ __('Start') }}
</a>
<a
:href="cls.join_url"
target="_blank"
class="w-full cursor-pointer inline-flex items-center justify-center gap-2 transition-colors focus:outline-none text-ink-gray-8 bg-surface-gray-2 hover:bg-surface-gray-3 active:bg-surface-gray-4 focus-visible:ring focus-visible:ring-outline-gray-3 h-7 text-base px-2 rounded"
>
<Video class="h-4 w-4 stroke-1.5" />
{{ __('Join') }}
</a>
</div>
<Tooltip
v-else-if="hasClassEnded(cls)"
:text="__('This class has ended')"
placement="right"
>
<div class="flex items-center space-x-2 text-ink-amber-3 w-fit">
<Info class="w-4 h-4 stroke-1.5" />
<span>
{{ __('Ended') }}
</span>
</div>
</Tooltip>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
+1 -1
View File
@@ -914,7 +914,7 @@ def give_discussions_permission():
"delete": 1,
"if_owner": 0 if role == "Moderator" else 1,
}
).save(ignore_permissions=True)
).save()
@frappe.whitelist()
@@ -9,18 +9,3 @@ from lms.lms.utils import has_course_instructor_role, has_moderator_role
class LMSAssignment(Document):
pass
@frappe.whitelist()
def save_assignment(assignment, title, type, question):
if not has_moderator_role() or not has_course_instructor_role():
return
if assignment:
doc = frappe.get_doc("LMS Assignment", assignment)
else:
doc = frappe.get_doc({"doctype": "LMS Assignment"})
doc.update({"title": title, "type": type, "question": question})
doc.save(ignore_permissions=True)
return doc.name
@@ -78,79 +78,3 @@ class LMSAssignmentSubmission(Document):
}
)
make_notification_logs(notification, [self.member])
@frappe.whitelist()
def upload_assignment(
assignment_attachment=None,
answer=None,
assignment=None,
lesson=None,
status="Not Graded",
comments=None,
submission=None,
):
if frappe.session.user == "Guest":
return
assignment_details = frappe.db.get_value(
"LMS Assignment", assignment, ["type", "grade_assignment"], as_dict=1
)
assignment_type = assignment_details.type
if assignment_type in ["URL", "Text"] and not answer:
frappe.throw(_("Please enter the URL for assignment submission."))
if assignment_type == "File" and not assignment_attachment:
frappe.throw(_("Please upload the assignment file."))
if assignment_type == "URL" and not validate_url(answer):
frappe.throw(_("Please enter a valid URL."))
if submission:
doc = frappe.get_doc("LMS Assignment Submission", submission)
else:
doc = frappe.get_doc(
{
"doctype": "LMS Assignment Submission",
"assignment": assignment,
"lesson": lesson,
"member": frappe.session.user,
"type": assignment_type,
}
)
doc.update(
{
"assignment_attachment": assignment_attachment,
"status": "Not Applicable"
if assignment_type == "Text" and not assignment_details.grade_assignment
else status,
"comments": comments,
"answer": answer,
}
)
doc.save(ignore_permissions=True)
return doc.name
@frappe.whitelist()
def get_assignment(lesson):
assignment = frappe.db.get_value(
"LMS Assignment Submission",
{"lesson": lesson, "member": frappe.session.user},
["name", "lesson", "member", "assignment_attachment", "comments", "status"],
as_dict=True,
)
assignment.file_name = frappe.db.get_value(
"File", {"file_url": assignment.assignment_attachment}, "file_name"
)
return assignment
@frappe.whitelist()
def grade_assignment(name, result, comments):
doc = frappe.get_doc("LMS Assignment Submission", name)
doc.status = result
doc.comments = comments
doc.save(ignore_permissions=True)
@@ -7,15 +7,3 @@ from frappe.model.document import Document
class LMSCourseInterest(Document):
pass
@frappe.whitelist()
def capture_interest(course):
data = {
"doctype": "LMS Course Interest",
"course": course,
"user": frappe.session.user,
}
if not frappe.db.exists(data):
frappe.get_doc(data).save(ignore_permissions=True)
return "OK"
@@ -20,16 +20,3 @@ class LMSCourseReview(Document):
def validate_if_already_reviewed(self):
if frappe.db.exists("LMS Course Review", {"course": self.course, "owner": self.owner}):
frappe.throw(_("You have already reviewed this course"))
@frappe.whitelist()
def submit_review(rating, review, course):
out_of_ratings = frappe.db.get_all(
"DocField", {"parent": "LMS Course Review", "fieldtype": "Rating"}, ["options"]
)
out_of_ratings = (len(out_of_ratings) and out_of_ratings[0].options) or 5
rating = cint(rating) / out_of_ratings
frappe.get_doc(
{"doctype": "LMS Course Review", "rating": rating, "review": review, "course": course}
).save(ignore_permissions=True)
return "OK"
@@ -93,18 +93,3 @@ def get_correct_options(question):
correct_options.append(field)
return correct_options
@frappe.whitelist()
def get_question_details(question):
if not has_course_instructor_role() or not has_moderator_role():
return
fields = ["question", "type", "name"]
for i in range(1, 5):
fields.append(f"option_{i}")
fields.append(f"is_correct_{i}")
fields.append(f"explanation_{i}")
fields.append(f"possibility_{i}")
return frappe.db.get_value("LMS Question", question, fields, as_dict=1)
-14
View File
@@ -257,20 +257,6 @@ def save_progress_after_quiz(quiz_details, percentage):
save_progress(quiz_details.lesson, quiz_details.course)
@frappe.whitelist()
def get_question_details(question):
if frappe.db.exists("LMS Quiz Question", question):
fields = ["name", "question", "type"]
for num in range(1, 5):
fields.append(f"option_{cstr(num)}")
fields.append(f"is_correct_{cstr(num)}")
fields.append(f"explanation_{cstr(num)}")
fields.append(f"possibility_{cstr(num)}")
return frappe.db.get_value("LMS Quiz Question", question, fields, as_dict=1)
return
@frappe.whitelist()
def check_answer(question, type, answers):
answers = json.loads(answers)
+1 -3
View File
@@ -10,9 +10,7 @@ import frappe
class TestLMSQuiz(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
frappe.get_doc({"doctype": "LMS Quiz", "title": "Test Quiz", "passing_percentage": 90}).save(
ignore_permissions=True
)
frappe.get_doc({"doctype": "LMS Quiz", "title": "Test Quiz", "passing_percentage": 90}).save()
def test_with_multiple_options(self):
question = frappe.new_doc("LMS Question")
+1 -40
View File
@@ -661,45 +661,6 @@ def get_evaluator(course, batch=None):
return evaluator
@frappe.whitelist()
def get_upcoming_evals(courses=None, batch=None):
if not courses:
courses = []
filters = {
"member": frappe.session.user,
"date": [">=", frappe.utils.nowdate()],
"status": "Upcoming",
}
if len(courses) > 0:
filters["course"] = ["in", courses]
if batch:
filters["batch_name"] = batch
upcoming_evals = frappe.get_all(
"LMS Certificate Request",
filters,
[
"name",
"date",
"start_time",
"course",
"evaluator",
"google_meet_link",
"member",
"member_name",
],
order_by="date",
)
for evals in upcoming_evals:
evals.course_title = frappe.db.get_value("LMS Course", evals.course, "title")
evals.evaluator_name = frappe.db.get_value("User", evals.evaluator, "full_name")
return upcoming_evals
def check_multicurrency(amount, currency, country=None, amount_usd=None):
settings = frappe.get_single("LMS Settings")
show_usd_equivalent = settings.show_usd_equivalent
@@ -1897,7 +1858,7 @@ def get_lesson_creation_details(course, chapter, lesson):
@frappe.whitelist()
def get_roles(name):
frappe.only_for("Moderator")
frappe.only_for(["Moderator", "Batch Evaluator"])
return {
"moderator": has_moderator_role(name),
"course_creator": has_course_instructor_role(name),