Merge branch 'develop' of https://github.com/frappe/lms into video-player
This commit is contained in:
@@ -60,7 +60,7 @@ const iconQuery = ref('')
|
|||||||
const selectedIcon = ref('')
|
const selectedIcon = ref('')
|
||||||
const search = ref(null)
|
const search = ref(null)
|
||||||
const emit = defineEmits(['update:modelValue', 'change'])
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
console.log(icons)
|
|
||||||
const iconArray = ref(
|
const iconArray = ref(
|
||||||
Object.keys(icons)
|
Object.keys(icons)
|
||||||
.sort(() => 0.5 - Math.random())
|
.sort(() => 0.5 - Math.random())
|
||||||
@@ -84,7 +84,6 @@ const props = defineProps({
|
|||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
selectedIcon.value = props.modelValue
|
selectedIcon.value = props.modelValue
|
||||||
console.log(search.value)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const setIcon = (icon, close) => {
|
const setIcon = (icon, close) => {
|
||||||
@@ -111,9 +110,5 @@ const filteredIcons = computed(() => {
|
|||||||
|
|
||||||
const openPopover = (togglePopover) => {
|
const openPopover = (togglePopover) => {
|
||||||
togglePopover()
|
togglePopover()
|
||||||
nextTick(() => {
|
|
||||||
/* search.value.focus() */
|
|
||||||
console.log(search.value.children)
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -377,7 +377,7 @@ const submitCourse = () => {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
onError(err) {
|
onError(err) {
|
||||||
showToast(err)
|
showToast('Error', err.messages?.[0] || err, 'x')
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,26 +69,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import {
|
import { Breadcrumbs, FormControl, createResource, Button } from 'frappe-ui'
|
||||||
Breadcrumbs,
|
|
||||||
FormControl,
|
|
||||||
createResource,
|
|
||||||
Button,
|
|
||||||
createDocumentResource,
|
|
||||||
} from 'frappe-ui'
|
|
||||||
import { computed, reactive, onMounted, inject, ref, watch } from 'vue'
|
import { computed, reactive, onMounted, inject, ref, watch } from 'vue'
|
||||||
import EditorJS from '@editorjs/editorjs'
|
import EditorJS from '@editorjs/editorjs'
|
||||||
import { createToast } from '../utils'
|
import { createToast } from '../utils'
|
||||||
import LessonPlugins from '@/components/LessonPlugins.vue'
|
import LessonPlugins from '@/components/LessonPlugins.vue'
|
||||||
import { getEditorTools } from '../utils'
|
import { getEditorTools } from '../utils'
|
||||||
import { ChevronRight } from 'lucide-vue-next'
|
import { ChevronRight } from 'lucide-vue-next'
|
||||||
import { useRouter } from 'vue-router'
|
|
||||||
|
|
||||||
const editor = ref(null)
|
const editor = ref(null)
|
||||||
const instructorEditor = ref(null)
|
const instructorEditor = ref(null)
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const openInstructorEditor = ref(false)
|
const openInstructorEditor = ref(false)
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
courseName: {
|
courseName: {
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-20">
|
<div class="mt-20">
|
||||||
<Discussions
|
<Discussions
|
||||||
v-if="allowDiscussions()"
|
v-if="allowDiscussions"
|
||||||
:title="'Questions'"
|
:title="'Questions'"
|
||||||
:doctype="'Course Lesson'"
|
:doctype="'Course Lesson'"
|
||||||
:docname="lesson.data.name"
|
:docname="lesson.data.name"
|
||||||
@@ -185,7 +185,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { createResource, Breadcrumbs, Button } from 'frappe-ui'
|
import { createResource, Breadcrumbs, Button } from 'frappe-ui'
|
||||||
import { computed, watch, ref, inject, createApp } from 'vue'
|
import { computed, watch, inject, ref } from 'vue'
|
||||||
import CourseOutline from '@/components/CourseOutline.vue'
|
import CourseOutline from '@/components/CourseOutline.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
@@ -198,7 +198,9 @@ import CourseInstructors from '@/components/CourseInstructors.vue'
|
|||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
let editor, instructorEditor
|
const allowDiscussions = ref(false)
|
||||||
|
const editor = ref(null)
|
||||||
|
const instructorEditor = ref(null)
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
courseName: {
|
courseName: {
|
||||||
@@ -228,13 +230,21 @@ const lesson = createResource({
|
|||||||
auto: true,
|
auto: true,
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
markProgress(data)
|
markProgress(data)
|
||||||
|
if (data.content) editor.value = renderEditor('editor', data.content)
|
||||||
if (data.content) editor = renderEditor('editor', data.content)
|
|
||||||
if (data.instructor_content?.blocks?.length)
|
if (data.instructor_content?.blocks?.length)
|
||||||
instructorEditor = renderEditor(
|
instructorEditor.value = renderEditor(
|
||||||
'instructor-content',
|
'instructor-content',
|
||||||
data.instructor_content
|
data.instructor_content
|
||||||
)
|
)
|
||||||
|
editor.value?.isReady.then(() => {
|
||||||
|
checkIfDiscussionsAllowed()
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!editor.value && data.body) {
|
||||||
|
const quizRegex = /\{\{ Quiz\(".*"\) \}\}/
|
||||||
|
const hasQuiz = quizRegex.test(data.body)
|
||||||
|
if (!hasQuiz) allowDiscussions.value = true
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -292,6 +302,9 @@ watch(
|
|||||||
[oldChapterNumber, oldLessonNumber]
|
[oldChapterNumber, oldLessonNumber]
|
||||||
) => {
|
) => {
|
||||||
if (newChapterNumber || newLessonNumber) {
|
if (newChapterNumber || newLessonNumber) {
|
||||||
|
editor.value = null
|
||||||
|
instructorEditor.value = null
|
||||||
|
allowDiscussions.value = false
|
||||||
lesson.submit({
|
lesson.submit({
|
||||||
chapter: newChapterNumber,
|
chapter: newChapterNumber,
|
||||||
lesson: newLessonNumber,
|
lesson: newLessonNumber,
|
||||||
@@ -300,12 +313,19 @@ watch(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const allowDiscussions = () => {
|
const checkIfDiscussionsAllowed = () => {
|
||||||
return (
|
let quizPresent = false
|
||||||
lesson.data?.membership ||
|
JSON.parse(lesson.data?.content)?.blocks?.forEach((block) => {
|
||||||
user.data?.is_moderator ||
|
if (block.type === 'quiz') quizPresent = true
|
||||||
user.data?.is_instructor
|
})
|
||||||
|
|
||||||
|
if (
|
||||||
|
!quizPresent &&
|
||||||
|
(lesson.data?.membership ||
|
||||||
|
user.data?.is_moderator ||
|
||||||
|
user.data?.is_instructor)
|
||||||
)
|
)
|
||||||
|
allowDiscussions.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
const allowEdit = () => {
|
const allowEdit = () => {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Breadcrumbs, createResource, Button, TabButtons } from 'frappe-ui'
|
import { Breadcrumbs, createResource, Button, TabButtons } from 'frappe-ui'
|
||||||
import { computed, inject, reactive, ref, onMounted, watchEffect } from 'vue'
|
import { computed, inject, watch, ref, onMounted, watchEffect } from 'vue'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { Edit } from 'lucide-vue-next'
|
import { Edit } from 'lucide-vue-next'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
@@ -119,11 +119,13 @@ onMounted(() => {
|
|||||||
|
|
||||||
const profile = createResource({
|
const profile = createResource({
|
||||||
url: 'frappe.client.get',
|
url: 'frappe.client.get',
|
||||||
params: {
|
makeParams(values) {
|
||||||
doctype: 'User',
|
return {
|
||||||
filters: {
|
doctype: 'User',
|
||||||
username: props.username,
|
filters: {
|
||||||
},
|
username: props.username,
|
||||||
|
},
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -165,6 +167,13 @@ watchEffect(() => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.username,
|
||||||
|
() => {
|
||||||
|
profile.reload()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const editProfile = () => {
|
const editProfile = () => {
|
||||||
showProfileModal.value = true
|
showProfileModal.value = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,12 @@
|
|||||||
{{ __('No introduction') }}
|
{{ __('No introduction') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-7 mb-10">
|
<div class="mt-7 mb-10" v-if="badges.data">
|
||||||
<h2 class="mb-3 text-lg font-semibold text-gray-900">
|
<h2 class="mb-3 text-lg font-semibold text-gray-900">
|
||||||
{{ __('Achievements') }}
|
{{ __('Achievements') }}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="grid grid-cols-5 gap-4">
|
<div class="grid grid-cols-5 gap-4">
|
||||||
<div v-if="badges.data" v-for="badge in badges.data">
|
<div v-for="badge in badges.data">
|
||||||
<Popover trigger="hover" :leaveDelay="Number(0.01)">
|
<Popover trigger="hover" :leaveDelay="Number(0.01)">
|
||||||
<template #target>
|
<template #target>
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
@@ -67,18 +67,24 @@
|
|||||||
size="sm"
|
size="sm"
|
||||||
@click="shareOnSocial(badge, 'LinkedIn')"
|
@click="shareOnSocial(badge, 'LinkedIn')"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #prefix>
|
||||||
<LinkedinIcon
|
<LinkedinIcon class="h-3 w-3 text-gray-700" />
|
||||||
class="h-3 w-3 stroke-1.5 text-gray-700"
|
|
||||||
/>
|
|
||||||
</template>
|
</template>
|
||||||
|
<span class="text-xs">
|
||||||
|
{{ __('LinkedIn') }}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
@click="shareOnSocial(badge, 'Twitter')"
|
@click="shareOnSocial(badge, 'Twitter')"
|
||||||
>
|
>
|
||||||
<Twitter class="h-3 w-3 stroke-1.5 text-gray-700" />
|
<template #prefix>
|
||||||
|
<Twitter class="h-3 w-3 text-gray-700" />
|
||||||
|
</template>
|
||||||
|
<span class="text-xs">
|
||||||
|
{{ __('Twitter') }}
|
||||||
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,6 +14,16 @@ frappe.ui.form.on("LMS Certificate Request", {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (!frm.doc.google_meet_link) {
|
||||||
|
frm.add_custom_button(__("Generate Google Meet Link"), () => {
|
||||||
|
frappe.call({
|
||||||
|
method: "lms.lms.doctype.lms_certificate_request.lms_certificate_request.setup_calendar_event",
|
||||||
|
args: {
|
||||||
|
eval: frm.doc,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
onload: function (frm) {
|
onload: function (frm) {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from frappe.utils import (
|
|||||||
get_time,
|
get_time,
|
||||||
)
|
)
|
||||||
from lms.lms.utils import get_evaluator
|
from lms.lms.utils import get_evaluator
|
||||||
|
import json
|
||||||
|
|
||||||
|
|
||||||
class LMSCertificateRequest(Document):
|
class LMSCertificateRequest(Document):
|
||||||
@@ -109,17 +110,21 @@ class LMSCertificateRequest(Document):
|
|||||||
|
|
||||||
def schedule_evals():
|
def schedule_evals():
|
||||||
if frappe.db.get_single_value("LMS Settings", "send_calendar_invite_for_evaluations"):
|
if frappe.db.get_single_value("LMS Settings", "send_calendar_invite_for_evaluations"):
|
||||||
one_hour_ago = add_to_date(get_datetime(), hours=-1)
|
timelapse = add_to_date(get_datetime(), hours=-5)
|
||||||
evals = frappe.get_all(
|
evals = frappe.get_all(
|
||||||
"LMS Certificate Request",
|
"LMS Certificate Request",
|
||||||
{"creation": [">=", one_hour_ago], "google_meet_link": ["is", "not set"]},
|
{"creation": [">=", timelapse], "google_meet_link": ["is", "not set"]},
|
||||||
["name", "member", "member_name", "evaluator", "date", "start_time", "end_time"],
|
["name", "member", "member_name", "evaluator", "date", "start_time", "end_time"],
|
||||||
)
|
)
|
||||||
for eval in evals:
|
for eval in evals:
|
||||||
setup_calendar_event(eval)
|
setup_calendar_event(eval)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
def setup_calendar_event(eval):
|
def setup_calendar_event(eval):
|
||||||
|
if isinstance(eval, str):
|
||||||
|
eval = frappe._dict(json.loads(eval))
|
||||||
|
|
||||||
calendar = frappe.db.get_value(
|
calendar = frappe.db.get_value(
|
||||||
"Google Calendar", {"user": eval.evaluator, "enable": 1}, "name"
|
"Google Calendar", {"user": eval.evaluator, "enable": 1}, "name"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,10 +15,10 @@
|
|||||||
<meta name="twitter:title" content="{{ meta.title }}" />
|
<meta name="twitter:title" content="{{ meta.title }}" />
|
||||||
<meta name="twitter:image" content="{{ meta.image }}" />
|
<meta name="twitter:image" content="{{ meta.image }}" />
|
||||||
<meta name="twitter:description" content="{{ meta.description }}" />
|
<meta name="twitter:description" content="{{ meta.description }}" />
|
||||||
<script type="module" crossorigin src="/assets/lms/frontend/assets/index-Cn4HoVlw.js"></script>
|
<script type="module" crossorigin src="/assets/lms/frontend/assets/index-kSywU9PY.js"></script>
|
||||||
<link rel="modulepreload" crossorigin href="/assets/lms/frontend/assets/frappe-ui-C28JHUMc.js">
|
<link rel="modulepreload" crossorigin href="/assets/lms/frontend/assets/frappe-ui-vM9kBbGH.js">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/frappe-ui-DzKBfka9.css">
|
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/frappe-ui-DzKBfka9.css">
|
||||||
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/index-BUt7GESC.css">
|
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/index-CxRhs9Fi.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.utils.telemetry import capture
|
from frappe.utils.telemetry import capture
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from bs4 import BeautifulSoup
|
||||||
import re
|
import re
|
||||||
|
|
||||||
no_cache = 1
|
no_cache = 1
|
||||||
@@ -79,6 +80,22 @@ def get_meta(app_path):
|
|||||||
"link": f"/batches/details/{batch_name}",
|
"link": f"/batches/details/{batch_name}",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if re.match(r"^batches/.*$", app_path):
|
||||||
|
batch_name = app_path.split("/")[1]
|
||||||
|
batch = frappe.db.get_value(
|
||||||
|
"LMS Batch",
|
||||||
|
batch_name,
|
||||||
|
["title", "meta_image", "description", "category", "medium"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"title": batch.title,
|
||||||
|
"image": batch.meta_image,
|
||||||
|
"description": batch.description,
|
||||||
|
"keywords": f"{batch.category} {batch.medium}",
|
||||||
|
"link": f"/batches/{batch_name}",
|
||||||
|
}
|
||||||
|
|
||||||
if app_path == "job-openings":
|
if app_path == "job-openings":
|
||||||
return {
|
return {
|
||||||
"title": _("Job Openings"),
|
"title": _("Job Openings"),
|
||||||
@@ -123,6 +140,10 @@ def get_meta(app_path):
|
|||||||
["full_name", "user_image", "bio"],
|
["full_name", "user_image", "bio"],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
soup = BeautifulSoup(user.bio, "html.parser")
|
||||||
|
user.bio = soup.get_text()
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"title": user.full_name,
|
"title": user.full_name,
|
||||||
"image": user.user_image,
|
"image": user.user_image,
|
||||||
|
|||||||
Reference in New Issue
Block a user