mirror of
https://github.com/frappe/lms.git
synced 2026-05-02 13:39:31 +03:00
@@ -216,7 +216,7 @@ const video_link = computed(() => {
|
||||
|
||||
function enrollStudent() {
|
||||
if (!user.data) {
|
||||
toast.success(__('You need to login first to enroll for this course'))
|
||||
toast.warning(__('You need to login first to enroll for this course'))
|
||||
setTimeout(() => {
|
||||
window.location.href = `/login?redirect-to=${window.location.pathname}`
|
||||
}, 500)
|
||||
|
||||
@@ -107,7 +107,11 @@
|
||||
v-model:reloadLiveClasses="liveClasses"
|
||||
/>
|
||||
|
||||
<LiveClassAttendance v-model="showAttendance" :live_class="attendanceFor" />
|
||||
<LiveClassAttendance
|
||||
v-if="showAttendance"
|
||||
v-model="showAttendance"
|
||||
:live_class="attendanceFor"
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createListResource, Button, Tooltip } from 'frappe-ui'
|
||||
|
||||
@@ -22,7 +22,10 @@
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip :text="__('Course')">
|
||||
<div class="flex items-center space-x-2 w-fit">
|
||||
<div
|
||||
class="flex space-x-2 w-fit cursor-pointer"
|
||||
@click="openLink('course', event.course)"
|
||||
>
|
||||
<BookOpen class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ event.course_title }}
|
||||
@@ -30,7 +33,10 @@
|
||||
</div>
|
||||
</Tooltip>
|
||||
<Tooltip v-if="event.batch_title" :text="__('Batch')">
|
||||
<div class="flex items-center space-x-2 w-fit">
|
||||
<div
|
||||
class="flex space-x-2 w-fit cursor-pointer"
|
||||
@click="openLink('batch', event.batch_name)"
|
||||
>
|
||||
<Users class="h-4 w-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ event.batch_title }}
|
||||
@@ -334,7 +340,7 @@ const certificateDetails = createResource({
|
||||
}
|
||||
},
|
||||
onError(err) {
|
||||
certificate.template = defaultTemplate.data.value
|
||||
certificate.template = defaultTemplate.data?.value
|
||||
},
|
||||
auto: false,
|
||||
})
|
||||
@@ -377,6 +383,16 @@ const openCertificate = (certificate) => {
|
||||
)
|
||||
}
|
||||
|
||||
const openLink = (type, name) => {
|
||||
let url = ''
|
||||
if (type === 'course') {
|
||||
url = `/lms/courses/${name}`
|
||||
} else if (type === 'batch') {
|
||||
url = `/lms/batches/${name}#students`
|
||||
}
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
const statusOptions = computed(() => {
|
||||
return [
|
||||
{
|
||||
|
||||
@@ -190,7 +190,7 @@ const evaluationCourses = computed(() => {
|
||||
|
||||
const canScheduleEvals = computed(() => {
|
||||
return (
|
||||
upcoming_evals.data?.length != evaluationCourses.length &&
|
||||
upcoming_evals.data?.length != evaluationCourses.value?.length &&
|
||||
!props.forHome &&
|
||||
!endDateHasPassed.value
|
||||
)
|
||||
|
||||
@@ -130,7 +130,6 @@
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
call,
|
||||
createListResource,
|
||||
Dropdown,
|
||||
FormControl,
|
||||
@@ -185,24 +184,27 @@ const batches = createListResource({
|
||||
cache: ['batches', user.data?.name],
|
||||
pageLength: pageLength.value,
|
||||
start: start.value,
|
||||
onSuccess(data) {
|
||||
let allCategories = data.map((batch) => batch.category)
|
||||
allCategories = allCategories.filter(
|
||||
(category, index) => allCategories.indexOf(category) === index && category
|
||||
)
|
||||
if (categories.value.length <= allCategories.length) {
|
||||
updateCategories(data)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const setCategories = (data) => {
|
||||
let allCategories = data.map((batch) => batch.category)
|
||||
allCategories = allCategories.filter(
|
||||
(category, index) => allCategories.indexOf(category) === index && category
|
||||
)
|
||||
if (categories.value.length <= allCategories.length) {
|
||||
updateCategories(data)
|
||||
}
|
||||
}
|
||||
|
||||
const updateBatches = () => {
|
||||
updateFilters()
|
||||
batches.update({
|
||||
filters: filters.value,
|
||||
orderBy: orderBy.value,
|
||||
})
|
||||
batches.reload()
|
||||
batches.reload().then((data) => {
|
||||
setCategories(data)
|
||||
})
|
||||
}
|
||||
|
||||
const updateFilters = () => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
||||
>
|
||||
<Breadcrumbs :items="breadcrumbs" />
|
||||
<router-link :to="{ name: 'Batches', query: { certification: true } }">
|
||||
<router-link :to="{ name: 'Courses', query: { certification: true } }">
|
||||
<Button>
|
||||
<template #prefix>
|
||||
<GraduationCap class="h-4 w-4 stroke-1.5" />
|
||||
|
||||
@@ -168,9 +168,6 @@ const courses = createListResource({
|
||||
cache: ['courses', user.data?.name],
|
||||
pageLength: pageLength.value,
|
||||
start: start.value,
|
||||
onSuccess(data) {
|
||||
setCategories(data)
|
||||
},
|
||||
})
|
||||
|
||||
const setCategories = (data) => {
|
||||
@@ -205,7 +202,7 @@ const identifyUserPersona = async () => {
|
||||
|
||||
const getCourseCount = () => {
|
||||
if (!user.data) return
|
||||
|
||||
if (!user.data.is_moderator) return
|
||||
call('frappe.client.get_count', {
|
||||
doctype: 'LMS Course',
|
||||
}).then((data) => {
|
||||
@@ -219,7 +216,9 @@ const updateCourses = () => {
|
||||
courses.update({
|
||||
filters: filters.value,
|
||||
})
|
||||
courses.reload()
|
||||
courses.reload().then((data) => {
|
||||
setCategories(data)
|
||||
})
|
||||
}
|
||||
|
||||
const updateFilters = () => {
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
<Uploader
|
||||
v-model="job.company_logo"
|
||||
:label="__('Company Logo')"
|
||||
:required="false"
|
||||
:required="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
class="flex space-x-2 px-2 py-4"
|
||||
:class="{
|
||||
'cursor-pointer': log.link,
|
||||
'items-center': !showDetails(log) && !isMention(log),
|
||||
'items-center': !showDetails(log) && !isMentionOrComment(log),
|
||||
}"
|
||||
@click="navigateToPage(log)"
|
||||
>
|
||||
@@ -56,9 +56,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="isMention(log)"
|
||||
v-if="isMentionOrComment(log)"
|
||||
v-html="log.email_content"
|
||||
class="bg-surface-gray-2 rounded-md px-3 py-2"
|
||||
class="bg-surface-gray-2 rounded-md px-3 py-2 line-clamp-3 overflow-hidden"
|
||||
></div>
|
||||
<div
|
||||
v-else-if="showDetails(log)"
|
||||
@@ -260,7 +260,7 @@ const navigateToPage = (log) => {
|
||||
}
|
||||
}
|
||||
|
||||
const isMention = (log) => {
|
||||
const isMentionOrComment = (log) => {
|
||||
if (log.type == 'Mention') {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -21,5 +21,5 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [require('@tailwindcss/line-clamp')],
|
||||
}
|
||||
|
||||
+57
@@ -0,0 +1,57 @@
|
||||
import frappe
|
||||
|
||||
ALLOWED_PATHS = [
|
||||
"/api/method/ping",
|
||||
"/api/method/login",
|
||||
"/api/method/logout",
|
||||
"/api/method/frappe.core.doctype.communication.email.mark_email_as_seen",
|
||||
"/api/method/frappe.realtime.get_user_info",
|
||||
"/api/method/frappe.realtime.can_subscribe_doc",
|
||||
"/api/method/frappe.realtime.can_subscribe_doctype",
|
||||
"/api/method/frappe.realtime.has_permission",
|
||||
"/api/method/frappe.integrations.oauth2.authorize",
|
||||
"/api/method/frappe.integrations.oauth2.approve",
|
||||
"/api/method/frappe.integrations.oauth2.get_token",
|
||||
"/api/method/frappe.integrations.oauth2.openid_profile",
|
||||
"/api/method/frappe.website.doctype.web_page_view.web_page_view.make_view_log",
|
||||
"/api/method/upload_file",
|
||||
"/api/method/frappe.search.web_search",
|
||||
"/api/method/frappe.email.queue.unsubscribe",
|
||||
"/api/method/frappe.website.doctype.web_form.web_form.accept",
|
||||
"/api/method/frappe.core.doctype.user.user.test_password_strength",
|
||||
"/api/method/frappe.core.doctype.user.user.update_password",
|
||||
"/api/method/frappe.utils.telemetry.pulse.client.is_enabled",
|
||||
"/api/method/frappe.client.get_value",
|
||||
"/api/method/frappe.client.get_count",
|
||||
"/api/method/frappe.client.get",
|
||||
"/api/method/frappe.client.insert",
|
||||
"/api/method/frappe.client.set_value",
|
||||
"/api/method/frappe.client.delete",
|
||||
"/api/method/frappe.client.get_list",
|
||||
"/api/method/frappe.client.rename_doc",
|
||||
"/api/method/frappe.onboarding.get_onboarding_status",
|
||||
"/api/method/frappe.utils.print_format.download_pdf",
|
||||
"/api/method/frappe.desk.search.search_link",
|
||||
"/api/method/frappe.core.doctype.communication.email.make",
|
||||
]
|
||||
|
||||
|
||||
def authenticate():
|
||||
if frappe.form_dict.cmd:
|
||||
path = f"/api/method/{frappe.form_dict.cmd}"
|
||||
else:
|
||||
path = frappe.request.path
|
||||
|
||||
user_type = frappe.db.get_value("User", frappe.session.user, "user_type")
|
||||
if user_type == "System User":
|
||||
return
|
||||
|
||||
if not path.startswith("/api/"):
|
||||
return
|
||||
print("path", path)
|
||||
if path.startswith("/lms") or path.startswith("/api/method/lms."):
|
||||
return
|
||||
|
||||
if path in ALLOWED_PATHS:
|
||||
return
|
||||
frappe.throw(f"Access not allowed for this URL: {path}", frappe.PermissionError)
|
||||
@@ -262,3 +262,4 @@ add_to_apps_screen = [
|
||||
]
|
||||
|
||||
sqlite_search = ["lms.sqlite.LearningSearch"]
|
||||
auth_hooks = ["lms.auth.authenticate"]
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
from frappe.tests import UnitTestCase
|
||||
from frappe.utils import add_days, format_time, getdate
|
||||
|
||||
from lms.lms.doctype.course_evaluator.course_evaluator import get_schedule
|
||||
from lms.lms.doctype.course_evaluator.course_evaluator import get_schedule, get_schedule_range_end_date
|
||||
from lms.lms.test_utils import TestUtils
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ class TestCourseEvaluator(UnitTestCase):
|
||||
def test_schedule_dates(self):
|
||||
schedule = get_schedule(self.batch.courses[0].course, self.batch.name)
|
||||
first_date = self.calculated_first_date_of_schedule()
|
||||
last_date = self.calculated_last_date_of_schedule(first_date)
|
||||
last_date = self.calculated_last_date_of_schedule()
|
||||
self.assertEqual(getdate(schedule[0].get("date")), first_date)
|
||||
self.assertEqual(getdate(schedule[-1].get("date")), last_date)
|
||||
|
||||
@@ -51,17 +51,10 @@ class TestCourseEvaluator(UnitTestCase):
|
||||
first_date = add_days(today, offset_wednesday)
|
||||
return first_date
|
||||
|
||||
def calculated_last_date_of_schedule(self, first_date):
|
||||
last_day = add_days(getdate(), 56)
|
||||
offset_monday = (0 - last_day.weekday() + 7) % 7 # 0 for Monday
|
||||
offset_wednesday = (2 - last_day.weekday() + 7) % 7 # 2 for Wednesday
|
||||
|
||||
if offset_monday < offset_wednesday and offset_monday <= 4:
|
||||
last_day = add_days(last_day, offset_monday)
|
||||
elif offset_wednesday <= 4:
|
||||
last_day = add_days(last_day, offset_wednesday)
|
||||
else:
|
||||
last_day = add_days(last_day, min(offset_monday, offset_wednesday) + 7)
|
||||
def calculated_last_date_of_schedule(self):
|
||||
last_day = getdate(get_schedule_range_end_date(getdate(), self.batch.name))
|
||||
while last_day.weekday() not in (0, 2):
|
||||
last_day = add_days(last_day, -1)
|
||||
|
||||
return last_day
|
||||
|
||||
|
||||
+18
-17
@@ -421,7 +421,7 @@ def get_batch_details_for_notification(topic):
|
||||
users = []
|
||||
batch_title = frappe.db.get_value("LMS Batch", topic.reference_docname, "title")
|
||||
subject = _("New comment in batch {0}").format(batch_title)
|
||||
link = f"/lms/batches/{topic.reference_docname}"
|
||||
link = f"/lms/batches/{topic.reference_docname}#discussions"
|
||||
instructors = frappe.db.get_all(
|
||||
"Course Instructor",
|
||||
{"parenttype": "LMS Batch", "parent": topic.reference_docname},
|
||||
@@ -590,32 +590,24 @@ def get_chart_date_range(from_date, to_date):
|
||||
|
||||
def get_chart_filters(doctype, chart, datefield, from_date, to_date):
|
||||
version = get_frappe_version()
|
||||
if version.startswith("16."):
|
||||
filters = [([chart.document_type, "docstatus", "<", 2])]
|
||||
filters = filters + json.loads(chart.filters_json)
|
||||
filters.append([doctype, datefield, ">=", from_date])
|
||||
filters.append([doctype, datefield, "<=", to_date])
|
||||
else:
|
||||
if version.startswith("15.") or version.startswith("14."):
|
||||
filters = [([chart.document_type, "docstatus", "<", 2, False])]
|
||||
filters = filters + json.loads(chart.filters_json)
|
||||
filters.append([doctype, datefield, ">=", from_date, False])
|
||||
filters.append([doctype, datefield, "<=", to_date, False])
|
||||
else:
|
||||
filters = [([chart.document_type, "docstatus", "<", 2])]
|
||||
filters = filters + json.loads(chart.filters_json)
|
||||
filters.append([doctype, datefield, ">=", from_date])
|
||||
filters.append([doctype, datefield, "<=", to_date])
|
||||
|
||||
return filters
|
||||
|
||||
|
||||
def get_chart_details(doctype, datefield, value_field, chart, from_date, to_date):
|
||||
filters = get_chart_filters(doctype, chart, datefield, from_date, to_date)
|
||||
version = get_frappe_version()
|
||||
if version.startswith("16."):
|
||||
return frappe.db.get_all(
|
||||
doctype,
|
||||
fields=[datefield, {"SUM": value_field}, {"COUNT": "*"}],
|
||||
filters=filters,
|
||||
group_by=datefield,
|
||||
order_by=datefield,
|
||||
as_list=True,
|
||||
)
|
||||
else:
|
||||
if version.startswith("15.") or version.startswith("14."):
|
||||
return frappe.db.get_all(
|
||||
doctype,
|
||||
fields=[f"{datefield} as _unit", f"SUM({value_field})", "COUNT(*)"],
|
||||
@@ -624,6 +616,15 @@ def get_chart_details(doctype, datefield, value_field, chart, from_date, to_date
|
||||
order_by="_unit asc",
|
||||
as_list=True,
|
||||
)
|
||||
else:
|
||||
return frappe.db.get_all(
|
||||
doctype,
|
||||
fields=[datefield, {"SUM": value_field}, {"COUNT": "*"}],
|
||||
filters=filters,
|
||||
group_by=datefield,
|
||||
order_by=datefield,
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import frappe
|
||||
from frappe.tests import UnitTestCase
|
||||
from frappe.tests.test_api import FrappeAPITestCase
|
||||
|
||||
from lms.auth import authenticate
|
||||
from lms.lms.test_utils import TestUtils
|
||||
|
||||
|
||||
class TestAuth(FrappeAPITestCase):
|
||||
def setUp(self):
|
||||
self.normal_user = TestUtils.create_user(
|
||||
self, "normal-user@example.com", "Normal", "User", ["LMS Student"]
|
||||
)
|
||||
|
||||
def test_allowed_path(self):
|
||||
site_url = frappe.utils.get_site_url(frappe.local.site)
|
||||
headers = {"Authorization": "Bearer set_test_example_user"}
|
||||
url = site_url + "/api/method/lms.lms.utils.get_courses"
|
||||
response = self.get(
|
||||
url,
|
||||
headers=headers,
|
||||
)
|
||||
self.assertNotEqual(response.json.get("exc_type"), "PermissionError")
|
||||
|
||||
def test_not_allowed_path(self):
|
||||
site_url = frappe.utils.get_site_url(frappe.local.site)
|
||||
headers = {"Authorization": "Bearer set_test_example_user"}
|
||||
url = site_url + "/api/method/frappe.auth.get_logged_user"
|
||||
response = self.get(
|
||||
url,
|
||||
headers=headers,
|
||||
)
|
||||
self.assertEqual(response.json.get("exc_type"), "PermissionError")
|
||||
|
||||
def tearDown(self):
|
||||
frappe.delete_doc("User", self.normal_user.name)
|
||||
Reference in New Issue
Block a user