chore: resolved conflicts

This commit is contained in:
Jannat Patel
2024-08-12 10:02:22 +05:30
26 changed files with 1657 additions and 3356 deletions

View File

@@ -22,7 +22,7 @@ git config user.name "frappe-pr-bot"
echo "Setting the correct git remote..."
# Here, the git remote is a local file path by default. Let's change it to the upstream repo.
git remote add upstream https://github.com/frappe/lms.git
git remote set-url upstream https://github.com/frappe/lms.git
echo "Creating a new branch..."
isodate=$(date -u +"%Y-%m-%d")

32
.github/workflows/on_release.yml vendored Normal file
View File

@@ -0,0 +1,32 @@
name: Generate Semantic Release
on:
workflow_dispatch:
push:
branches:
- main
jobs:
release:
name: Release
runs-on: ubuntu-latest
steps:
- name: Checkout Entire Repository
uses: actions/checkout@v4
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
- name: Setup dependencies
run: |
npm install @semantic-release/git @semantic-release/exec --no-save
- name: Create Release
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GIT_AUTHOR_NAME: "Frappe PR Bot"
GIT_AUTHOR_EMAIL: "developers@frappe.io"
GIT_COMMITTER_NAME: "Frappe PR Bot"
GIT_COMMITTER_EMAIL: "developers@frappe.io"
run: npx semantic-release

39
.github/workflows/release_notes.yml vendored Normal file
View File

@@ -0,0 +1,39 @@
# This action:
#
# 1. Generates release notes using github API.
# 2. Strips unnecessary info like chore/style etc from notes.
# 3. Updates release info.
name: 'Release Notes'
on:
workflow_dispatch:
inputs:
tag_name:
description: 'Tag of release like v2.0.0'
required: true
type: string
release:
types: [released]
permissions:
contents: read
jobs:
regen-notes:
name: 'Regenerate release notes'
runs-on: ubuntu-latest
steps:
- name: Update notes
run: |
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/lms/releases/generate-notes -f tag_name=$RELEASE_TAG \
| jq -r '.body' \
| sed -E '/^\* (chore|ci|test|docs|style)/d' \
| sed -E 's/by @mergify //'
)
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/lms/releases/tags/$RELEASE_TAG | jq -r '.id')
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/lms/releases/$RELEASE_ID -f body="$NEW_NOTES"
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }}

21
.releaserc Normal file
View File

@@ -0,0 +1,21 @@
{
"branches": ["develop"],
"plugins": [
"@semantic-release/commit-analyzer", {
"preset": "angular"
},
"@semantic-release/release-notes-generator",
[
"@semantic-release/exec", {
"prepareCmd": 'sed -ir "s/[0-9]*\.[0-9]*\.[0-9]*/${nextRelease.version}/" lms/__init__.py'
}
],
[
"@semantic-release/git", {
"assets": ["lms/__init__.py"],
"message": "chore(release): Bumped to Version ${nextRelease.version}"
}
],
"@semantic-release/github"
]
}

View File

@@ -100,7 +100,7 @@ import { ChevronRight, Plus } from 'lucide-vue-next'
import { createResource, Button } from 'frappe-ui'
import PageModal from '@/components/Modals/PageModal.vue'
const { user } = sessionStore()
const { user, sidebarSettings } = sessionStore()
const { userResource } = usersStore()
const socket = inject('$socket')
const unreadCount = ref(0)
@@ -115,6 +115,20 @@ onMounted(() => {
unreadNotifications.reload()
})
addNotifications()
sidebarSettings.reload(
{},
{
onSuccess(data) {
Object.keys(data).forEach((key) => {
if (!parseInt(data[key])) {
sidebarLinks.value = sidebarLinks.value.filter(
(link) => link.label.toLowerCase().split(' ').join('_') !== key
)
}
})
},
}
)
})
const unreadNotifications = createResource({
@@ -153,21 +167,6 @@ const addNotifications = () => {
}
}
const sidebarSettings = createResource({
url: 'lms.lms.api.get_sidebar_settings',
cache: 'Sidebar Settings',
auto: true,
onSuccess(data) {
Object.keys(data).forEach((key) => {
if (!parseInt(data[key])) {
sidebarLinks.value = sidebarLinks.value.filter(
(link) => link.label.toLowerCase().split(' ').join('_') !== key
)
}
})
},
})
const openPageModal = (link) => {
showPageModal.value = true
pageToEdit.value = link

View File

@@ -3,7 +3,7 @@
class="flex flex-col shadow hover:bg-gray-100 rounded-md p-4 h-full"
style="min-height: 150px"
>
<div class="text-xl font-semibold mb-2">
<div class="text-lg leading-5 font-semibold mb-2">
{{ batch.title }}
</div>
<Badge
@@ -22,18 +22,17 @@
>
{{ __('Sold Out') }}
</Badge>
<div class="short-introduction">
<div class="short-introduction text-sm text-gray-700">
{{ batch.description }}
</div>
<div v-if="batch.amount" class="font-semibold mb-4">
{{ batch.price }}
</div>
<div class="flex flex-col space-y-2 mt-auto">
<div v-if="batch.amount" class="font-semibold text-lg">
{{ batch.price }}
</div>
<DateRange
:startDate="batch.start_date"
:endDate="batch.end_date"
class="text-sm text-gray-700 mb-3"
class="text-sm text-gray-700"
/>
<div class="flex items-center text-sm text-gray-700">
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
@@ -50,18 +49,21 @@
{{ batch.timezone }}
</span>
</div>
<div v-if="batch.instructors?.length" class="flex avatar-group overlap">
<div
class="h-6 mr-1"
:class="{ 'avatar-group overlap': batch.instructors.length > 1 }"
>
<UserAvatar
v-for="instructor in batch.instructors"
:user="instructor"
/>
</div>
<CourseInstructors :instructors="batch.instructors" />
</div>
<div
v-if="batch.instructors?.length"
class="flex avatar-group overlap mt-4"
>
<div
class="h-6 mr-1"
:class="{ 'avatar-group overlap': batch.instructors.length > 1 }"
>
<UserAvatar
v-for="instructor in batch.instructors"
:user="instructor"
/>
</div>
<CourseInstructors :instructors="batch.instructors" />
</div>
</div>
</template>
@@ -88,7 +90,7 @@ const props = defineProps({
text-overflow: ellipsis;
width: 100%;
overflow: hidden;
margin: 0.25rem 0 1.25rem;
margin: 0.25rem 0 1rem;
line-height: 1.5;
}

View File

@@ -72,7 +72,7 @@
{{ course.title }}
</div>
<div class="short-introduction">
<div class="short-introduction text-gray-700 text-sm">
{{ course.short_introduction }}
</div>

View File

@@ -50,7 +50,7 @@
<div class="outline-lesson pl-8 py-2 pr-4">
<router-link
:to="{
name: allowEdit ? 'CreateLesson' : 'Lesson',
name: allowEdit ? 'LessonForm' : 'Lesson',
params: {
courseName: courseName,
chapterNumber: lesson.number.split('.')[0],
@@ -89,7 +89,7 @@
<div v-if="allowEdit" class="flex mt-2 mb-4 pl-8">
<router-link
:to="{
name: 'CreateLesson',
name: 'LessonForm',
params: {
courseName: courseName,
chapterNumber: chapter.idx,

View File

@@ -4,14 +4,14 @@
<slot />
</div>
<div
v-if="tabs"
v-if="sidebarSettings.data"
class="fixed flex justify-around border-t border-gray-300 bottom-0 z-10 w-full bg-white standalone:pb-4"
:style="{
gridTemplateColumns: `repeat(${tabs.length}, minmax(0, 1fr))`,
gridTemplateColumns: `repeat(${sidebarLinks.length}, minmax(0, 1fr))`,
}"
>
<button
v-for="tab in tabs"
v-for="tab in sidebarLinks"
:key="tab.label"
:class="isVisible(tab) ? 'block' : 'hidden'"
class="flex flex-col items-center justify-center py-3 transition active:scale-95"
@@ -29,21 +29,38 @@
<script setup>
import { getSidebarLinks } from '../utils'
import { useRouter } from 'vue-router'
import { computed } from 'vue'
import { computed, ref, onMounted } from 'vue'
import { sessionStore } from '@/stores/session'
import { usersStore } from '@/stores/user'
import * as icons from 'lucide-vue-next'
const { logout, user } = sessionStore()
const { logout, user, sidebarSettings } = sessionStore()
let { isLoggedIn } = sessionStore()
const router = useRouter()
let { userResource } = usersStore()
const sidebarLinks = ref(getSidebarLinks())
const tabs = computed(() => {
let links = getSidebarLinks()
onMounted(() => {
sidebarSettings.reload(
{},
{
onSuccess(data) {
Object.keys(data).forEach((key) => {
if (!parseInt(data[key])) {
sidebarLinks.value = sidebarLinks.value.filter(
(link) => link.label.toLowerCase().split(' ').join('_') !== key
)
}
})
addAccessLinks()
},
}
)
})
const addAccessLinks = () => {
if (user) {
links.push({
sidebarLinks.value.push({
label: 'Profile',
icon: 'UserRound',
activeFor: [
@@ -54,18 +71,17 @@ const tabs = computed(() => {
'ProfileRoles',
],
})
links.push({
sidebarLinks.value.push({
label: 'Log out',
icon: 'LogOut',
})
} else {
links.push({
sidebarLinks.value.push({
label: 'Log in',
icon: 'LogIn',
})
}
return links
})
}
let isActive = (tab) => {
return tab.activeFor?.includes(router.currentRoute.value.name)

View File

@@ -3,9 +3,7 @@
<div class="bg-blue-100 py-2 px-2 mb-4 rounded-md text-sm text-blue-800">
<div class="leading-relaxed">
{{
__('This quiz consists of {0} questions.').format(
quiz.data.questions.length
)
__('This quiz consists of {0} questions.').format(questions.length)
}}
</div>
<div v-if="quiz.data.passing_percentage" class="leading-relaxed">
@@ -59,7 +57,7 @@
</div>
</div>
<div v-else-if="!quizSubmission.data">
<div v-for="(question, qtidx) in quiz.data.questions">
<div v-for="(question, qtidx) in questions">
<div
v-if="qtidx == activeQuestion - 1 && questionDetails.data"
class="border rounded-md p-5"
@@ -166,7 +164,7 @@
{{
__('Question {0} of {1}').format(
activeQuestion,
quiz.data.questions.length
questions.length
)
}}
</div>
@@ -179,7 +177,7 @@
</span>
</Button>
<Button
v-else-if="activeQuestion != quiz.data.questions.length"
v-else-if="activeQuestion != questions.length"
@click="nextQuetion()"
>
<span>
@@ -250,6 +248,7 @@ const activeQuestion = ref(0)
const currentQuestion = ref('')
const selectedOptions = reactive([0, 0, 0, 0])
const showAnswers = reactive([])
let questions = reactive([])
const possibleAnswer = ref(null)
const props = defineProps({
@@ -270,15 +269,30 @@ const quiz = createResource({
cache: ['quiz', props.quizName],
auto: true,
onSuccess(data) {
if (data.shuffle_questions) {
data.questions = data.questions.sort(() => Math.random() - 0.5)
}
if (data.limit_questions_to) {
data.questions = data.questions.slice(0, data.limit_questions_to)
}
populateQuestions()
},
})
const populateQuestions = () => {
let data = quiz.data
if (data.shuffle_questions) {
questions = shuffleArray(data.questions)
if (data.limit_questions_to) {
questions = questions.slice(0, data.limit_questions_to)
}
} else {
questions = data.questions
}
}
const shuffleArray = (array) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1))
;[array[i], array[j]] = [array[j], array[i]]
}
return array
}
const attempts = createResource({
url: 'frappe.client.get_list',
makeParams(values) {
@@ -310,7 +324,7 @@ const attempts = createResource({
watch(
() => quiz.data,
() => {
if (quiz.data) {
if (quiz.data && quiz.data.max_attempts) {
attempts.reload()
resetQuiz()
}
@@ -464,7 +478,7 @@ const submitQuiz = () => {
const createSubmission = () => {
quizSubmission.reload().then(() => {
attempts.reload()
if (quiz.data && quiz.data.max_attempts) attempts.reload()
})
}
@@ -473,6 +487,7 @@ const resetQuiz = () => {
selectedOptions.splice(0, selectedOptions.length, ...[0, 0, 0, 0])
showAnswers.length = 0
quizSubmission.reset()
populateQuestions()
}
const getSubmissionColumns = () => {

View File

@@ -5,7 +5,7 @@
>
<Breadcrumbs
class="h-7"
:items="[{ label: __('All Batches'), route: { name: 'Batches' } }]"
:items="[{ label: __('Batches'), route: { name: 'Batches' } }]"
/>
<div class="flex space-x-2">
<div class="w-40">
@@ -25,7 +25,7 @@
>
<Button variant="solid">
<template #prefix>
<Plus class="h-4 w-4" />
<Plus class="h-4 w-4 stroke-1.5" />
</template>
{{ __('New Batch') }}
</Button>

View File

@@ -6,9 +6,10 @@
<div>
<FormControl
type="text"
placeholder="Search Participants"
placeholder="Search"
v-model="searchQuery"
@input="participants.reload()"
class="w-40"
>
<template #prefix>
<Search class="w-4 stroke-1.5 text-gray-600" name="search" />

View File

@@ -5,19 +5,21 @@
>
<Breadcrumbs
class="h-7"
:items="[{ label: __('All Courses'), route: { name: 'Courses' } }]"
:items="[{ label: __('Courses'), route: { name: 'Courses' } }]"
/>
<div class="flex space-x-2">
<FormControl
type="text"
placeholder="Search Course"
v-model="searchQuery"
@input="courses.reload()"
>
<template #prefix>
<Search class="w-4 stroke-1.5 text-gray-600" name="search" />
</template>
</FormControl>
<div class="flex space-x-2 justify-end">
<div class="w-36">
<FormControl
type="text"
placeholder="Search"
v-model="searchQuery"
@input="courses.reload()"
>
<template #prefix>
<Search class="w-4 h-4 stroke-1.5" name="search" />
</template>
</FormControl>
</div>
<router-link
:to="{
name: 'CreateCourse',

View File

@@ -50,9 +50,9 @@
</Button>
</div>
</header>
<div v-if="job.data" class="w-3/4 mx-auto">
<div v-if="job.data" class="max-w-3xl mx-auto">
<div class="p-4">
<div class="flex mb-4">
<div class="flex mb-10">
<img
:src="job.data.company_logo"
class="w-16 h-16 rounded-lg object-contain mr-4"
@@ -62,40 +62,36 @@
<div class="text-2xl font-semibold mb-4">
{{ job.data.job_title }}
</div>
<div class="grid grid-cols-3 gap-8">
<div class="grid grid-cols-1 gap-2">
<div class="flex items-center space-x-2">
<Building2 class="h-4 w-4 stroke-1.5" />
<span>{{ job.data.company_name }}</span>
</div>
<div class="flex items-center space-x-2">
<MapPin class="h-4 w-4 stroke-1.5" />
<span>{{ job.data.location }}</span>
</div>
<div
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-x-10 gap-y-2 md:gap-y-4"
>
<div class="flex items-center space-x-2">
<Building2 class="h-4 w-4 stroke-1.5" />
<span>{{ job.data.company_name }}</span>
</div>
<div class="grid grid-cols-1 gap-2">
<div class="flex items-center space-x-2">
<ClipboardType class="h-4 w-4 stroke-1.5" />
<span>{{ job.data.type }}</span>
</div>
<div class="flex items-center space-x-2">
<CalendarDays class="h-4 w-4 stroke-1.5" />
<span>{{
dayjs(job.data.creation).format('DD MMM YYYY')
}}</span>
</div>
<div class="flex items-center space-x-2">
<MapPin class="h-4 w-4 stroke-1.5" />
<span>{{ job.data.location }}</span>
</div>
<div class="grid grid-cols-1 h-fit">
<div
v-if="applicationCount.data"
class="flex items-center space-x-2"
<div class="flex items-center space-x-2">
<ClipboardType class="h-4 w-4 stroke-1.5" />
<span>{{ job.data.type }}</span>
</div>
<div class="flex items-center space-x-2">
<CalendarDays class="h-4 w-4 stroke-1.5" />
<span>
{{ dayjs(job.data.creation).format('DD MMM YYYY') }}
</span>
</div>
<div
v-if="applicationCount.data"
class="flex items-center space-x-2"
>
<SquareUserRound class="h-4 w-4 stroke-1.5" />
<span
>{{ applicationCount.data }}
{{ __('applications received') }}</span
>
<SquareUserRound class="h-4 w-4 stroke-1.5" />
<span
>{{ applicationCount.data }}
{{ __('applications received') }}</span
>
</div>
</div>
</div>
</div>

View File

@@ -58,7 +58,7 @@
<router-link
v-if="allowEdit()"
:to="{
name: 'CreateLesson',
name: 'LessonForm',
params: {
courseName: courseName,
chapterNumber: props.chapterNumber,

View File

@@ -80,6 +80,7 @@ const editor = ref(null)
const instructorEditor = ref(null)
const user = inject('$user')
const openInstructorEditor = ref(false)
let autoSaveInterval
const props = defineProps({
courseName: {
@@ -134,32 +135,45 @@ const lessonDetails = createResource({
lesson[key] = data.lesson[key]
})
lesson.include_in_preview = data.include_in_preview ? true : false
editor.value.isReady.then(() => {
if (data.lesson.content) {
editor.value.render(JSON.parse(data.lesson.content))
} else if (data.lesson.body) {
let blocks = convertToJSON(data.lesson)
editor.value.render({
blocks: blocks,
})
}
})
instructorEditor.value.isReady.then(() => {
if (data.lesson.instructor_content) {
instructorEditor.value.render(
JSON.parse(data.lesson.instructor_content)
)
} else if (data.lesson.instructor_notes) {
let blocks = convertToJSON(data.lesson)
instructorEditor.value.render({
blocks: blocks,
})
}
})
addLessonContent(data)
addInstructorNotes(data)
enableAutoSave()
}
},
})
const addLessonContent = (data) => {
editor.value.isReady.then(() => {
if (data.lesson.content) {
editor.value.render(JSON.parse(data.lesson.content))
} else if (data.lesson.body) {
let blocks = convertToJSON(data.lesson)
editor.value.render({
blocks: blocks,
})
}
})
}
const addInstructorNotes = (data) => {
instructorEditor.value.isReady.then(() => {
if (data.lesson.instructor_content) {
instructorEditor.value.render(JSON.parse(data.lesson.instructor_content))
} else if (data.lesson.instructor_notes) {
let blocks = convertToJSON(data.lesson)
instructorEditor.value.render({
blocks: blocks,
})
}
})
}
const enableAutoSave = () => {
autoSaveInterval = setInterval(() => {
saveLesson()
}, 10000)
}
const newLessonResource = createResource({
url: 'frappe.client.insert',
makeParams(values) {
@@ -357,9 +371,6 @@ const editCurrentLesson = () => {
validate() {
return validateLesson()
},
onSuccess() {
showToast('Success', 'Lesson updated successfully', 'check')
},
onError(err) {
showToast('Error', err.message, 'x')
},
@@ -418,7 +429,7 @@ const breadcrumbs = computed(() => {
crumbs.push({
label: lessonDetails?.data?.lesson ? 'Edit Lesson' : 'Create Lesson',
route: {
name: 'CreateLesson',
name: 'LessonForm',
params: {
courseName: props.courseName,
chapterNumber: props.chapterNumber,

View File

@@ -16,7 +16,7 @@
<h2 class="mb-3 text-lg font-semibold text-gray-900">
{{ __('Achievements') }}
</h2>
<div class="grid grid-cols-5 gap-4">
<div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4">
<div v-for="badge in badges.data">
<Popover trigger="hover" :leaveDelay="Number(0.01)">
<template #target>

View File

@@ -103,8 +103,8 @@ const routes = [
},
{
path: '/courses/:courseName/learn/:chapterNumber-:lessonNumber/edit',
name: 'CreateLesson',
component: () => import('@/pages/CreateLesson.vue'),
name: 'LessonForm',
component: () => import('@/pages/LessonForm.vue'),
props: true,
},
{

View File

@@ -53,11 +53,18 @@ export const sessionStore = defineStore('lms-session', () => {
},
})
const sidebarSettings = createResource({
url: 'lms.lms.api.get_sidebar_settings',
cache: 'Sidebar Settings',
auto: false,
})
return {
user,
isLoggedIn,
login,
logout,
branding,
sidebarSettings,
}
})

View File

@@ -425,7 +425,7 @@ export function getSidebarLinks() {
'CourseDetail',
'Lesson',
'CreateCourse',
'CreateLesson',
'LessonForm',
],
},
{

View File

@@ -176,7 +176,16 @@ update_website_context = [
]
jinja = {
"methods": ["lms.lms.utils.get_signup_optin_checks"],
"methods": [
"lms.lms.utils.get_signup_optin_checks",
"lms.lms.utils.get_tags",
"lms.lms.utils.get_lesson_count",
"lms.lms.utils.get_instructors",
"lms.lms.utils.get_lesson_index",
"lms.lms.utils.get_lesson_url",
"lms.page_renderers.get_profile_url",
"lms.overrides.user.get_palette",
],
"filters": [],
}
## Specify the additional tabs to be included in the user profile page.

View File

@@ -59,6 +59,7 @@ class LMSCertificateRequest(Document):
"evaluator": self.evaluator,
"date": self.date,
"start_time": self.start_time,
"member": ["!=", self.member],
},
):
frappe.throw(_("The slot is already booked by another participant."))

View File

@@ -9,7 +9,7 @@
"label": "Enrollments"
}
],
"content": "[{\"id\":\"jNO4sdKxHu\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Get Started</b></span>\",\"col\":12}},{\"id\":\"5s0qRBc4rY\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/lms/courses\\\">Visit LMS Portal</a>\",\"col\":4}},{\"id\":\"lGMuNLpmv-\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/lms/courses/new/edit\\\">Create a Course</a>\",\"col\":4}},{\"id\":\"3TVyc9AkPy\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/web-page/new-web-page-1\\\">Setup a Home Page</a>\",\"col\":4}},{\"id\":\"9zcbqpu2gm\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/lms-settings/LMS%20Settings\\\">LMS Setting</a>\",\"col\":4}},{\"id\":\"0ATmnKmXjc\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"https://docs.frappe.io/learning\\\">Documentation</a>\",\"col\":4}},{\"id\":\"C128a4abjX\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"5q4sPiv2ci\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Signups\",\"col\":6}},{\"id\":\"8NSaRaEV5u\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Enrollments\",\"col\":6}},{\"id\":\"kMuzko0uAU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"iuvIOHmztI\",\"type\":\"header\",\"data\":{\"text\":\"<span style=\\\"font-size: 18px;\\\"><b>Statistics</b></span>\",\"col\":12}},{\"id\":\"l0VTd66Uy2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Users\",\"col\":4}},{\"id\":\"wAWZin1KKk\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course\",\"col\":4}},{\"id\":\"RLrIlFx0Hd\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Enrollments\",\"col\":4}},{\"id\":\"OuhWkhCQmq\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course Completed\",\"col\":4}},{\"id\":\"3g8QmNqUXG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Certificate\",\"col\":4}},{\"id\":\"EZsdsujs8N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Evaluation\",\"col\":4}},{\"id\":\"s-nfsFQbGV\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"jeOBWBzHEa\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Master</b></span>\",\"col\":12}},{\"id\":\"sVhgfS5GIh\",\"type\":\"card\",\"data\":{\"card_name\":\"Course Data\",\"col\":4}},{\"id\":\"Iea0snm4Fg\",\"type\":\"card\",\"data\":{\"card_name\":\"Course Stats\",\"col\":4}},{\"id\":\"bZB7RqOl6a\",\"type\":\"card\",\"data\":{\"card_name\":\"Certification\",\"col\":4}}]",
"content": "[{\"id\":\"jNO4sdKxHu\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Get Started</b></span>\",\"col\":12}},{\"id\":\"5s0qRBc4rY\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/lms/courses\\\">Visit LMS Portal</a>\",\"col\":4}},{\"id\":\"lGMuNLpmv-\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/lms/courses/new/edit\\\">Create a Course</a>\",\"col\":4}},{\"id\":\"3TVyc9AkPy\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/web-page/new-web-page-1\\\">Setup a Home Page</a>\",\"col\":4}},{\"id\":\"9zcbqpu2gm\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"/app/lms-settings/LMS%20Settings\\\">LMS Settings</a>\",\"col\":4}},{\"id\":\"0ATmnKmXjc\",\"type\":\"paragraph\",\"data\":{\"text\":\"<a href=\\\"https://docs.frappe.io/learning\\\">Documentation</a>\",\"col\":4}},{\"id\":\"C128a4abjX\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"5q4sPiv2ci\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Signups\",\"col\":6}},{\"id\":\"8NSaRaEV5u\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Enrollments\",\"col\":6}},{\"id\":\"kMuzko0uAU\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"iuvIOHmztI\",\"type\":\"header\",\"data\":{\"text\":\"<span style=\\\"font-size: 18px;\\\"><b>Statistics</b></span>\",\"col\":12}},{\"id\":\"l0VTd66Uy2\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Users\",\"col\":4}},{\"id\":\"wAWZin1KKk\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course\",\"col\":4}},{\"id\":\"RLrIlFx0Hd\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Enrollments\",\"col\":4}},{\"id\":\"OuhWkhCQmq\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Course Completed\",\"col\":4}},{\"id\":\"3g8QmNqUXG\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Certificate\",\"col\":4}},{\"id\":\"EZsdsujs8N\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Evaluation\",\"col\":4}},{\"id\":\"s-nfsFQbGV\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"jeOBWBzHEa\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Master</b></span>\",\"col\":12}},{\"id\":\"sVhgfS5GIh\",\"type\":\"card\",\"data\":{\"card_name\":\"Course Data\",\"col\":4}},{\"id\":\"Iea0snm4Fg\",\"type\":\"card\",\"data\":{\"card_name\":\"Course Stats\",\"col\":4}},{\"id\":\"bZB7RqOl6a\",\"type\":\"card\",\"data\":{\"card_name\":\"Certification\",\"col\":4}}]",
"creation": "2021-10-21 17:20:01.358903",
"custom_blocks": [],
"docstatus": 0,
@@ -145,7 +145,7 @@
"type": "Link"
}
],
"modified": "2024-06-27 21:19:06.273056",
"modified": "2024-08-09 13:19:06.273056",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS",
@@ -213,4 +213,4 @@
}
],
"title": "LMS"
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -26,5 +26,8 @@
"devDependencies": {
"cypress": "^13.9.0",
"cypress-file-upload": "^5.0.8"
},
"dependencies": {
"pre-commit": "^1.2.2"
}
}

514
yarn.lock

File diff suppressed because it is too large Load Diff