feat: learning paths
This commit is contained in:
@@ -183,6 +183,17 @@ const addQuizzes = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const addPrograms = () => {
|
||||
if (isInstructor.value || isModerator.value) {
|
||||
sidebarLinks.value.push({
|
||||
label: 'Programs',
|
||||
icon: 'Route',
|
||||
to: 'Programs',
|
||||
activeFor: ['Programs', 'ProgramForm'],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const openPageModal = (link) => {
|
||||
showPageModal.value = true
|
||||
pageToEdit.value = link
|
||||
@@ -215,6 +226,7 @@ watch(userResource, () => {
|
||||
isModerator.value = userResource.data.is_moderator
|
||||
isInstructor.value = userResource.data.is_instructor
|
||||
addQuizzes()
|
||||
addPrograms()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -108,9 +108,31 @@ const tabsStructure = computed(() => {
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Members',
|
||||
description: 'Manage the members of your learning system',
|
||||
icon: 'UserRoundPlus',
|
||||
label: 'General',
|
||||
icon: 'Wrench',
|
||||
fields: [
|
||||
{
|
||||
label: 'Enable Learning Paths',
|
||||
name: 'enable_learning_paths',
|
||||
description:
|
||||
'This will change the default flow of the system and enforce students to go through programs assigned to them in the correct order.',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Send calendar invite for evaluations',
|
||||
name: 'send_calendar_invite_for_evaluations',
|
||||
description:
|
||||
'If enabled and Google Calendar of the evaluator is set in the system, students will receive calendar invites to remind them of their evaluations.',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Unsplash Access Key',
|
||||
name: 'unsplash_access_key',
|
||||
description:
|
||||
'Optional. If this is set, students can pick a cover image from the unsplash library for their profile page. Refer the docs to know more https://unsplash.com/documentation#getting-started.',
|
||||
type: 'text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -156,9 +178,14 @@ const tabsStructure = computed(() => {
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
hideLabel: true,
|
||||
label: 'Lists',
|
||||
hideLabel: false,
|
||||
items: [
|
||||
{
|
||||
label: 'Members',
|
||||
description: 'Manage the members of your learning system',
|
||||
icon: 'UserRoundPlus',
|
||||
},
|
||||
{
|
||||
label: 'Categories',
|
||||
description: 'Manage the members of your learning system',
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
<script setup>
|
||||
import { Button, Badge } from 'frappe-ui'
|
||||
import SettingFields from '@/components/SettingFields.vue'
|
||||
import { showToast } from '@/utils'
|
||||
|
||||
const props = defineProps({
|
||||
fields: {
|
||||
@@ -54,7 +55,14 @@ const update = () => {
|
||||
props.data.doc[f.name] = f.value
|
||||
}
|
||||
})
|
||||
props.data.save.submit()
|
||||
props.data.save.submit(
|
||||
{},
|
||||
{
|
||||
onError(err) {
|
||||
showToast('Error', err.messages?.[0] || err, 'x')
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@
|
||||
:type="field.type"
|
||||
:rows="field.rows"
|
||||
:options="field.options"
|
||||
:description="field.description"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,7 +101,7 @@
|
||||
import { FormControl, FileUploader, Button, Switch } from 'frappe-ui'
|
||||
import { computed } from 'vue'
|
||||
import { getFileSize, validateFile } from '@/utils'
|
||||
import { X, FileText } from 'lucide-vue-next'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import CodeEditor from '@/components/Controls/CodeEditor.vue'
|
||||
|
||||
|
||||
88
frontend/src/pages/ProgramForm.vue
Normal file
88
frontend/src/pages/ProgramForm.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<header
|
||||
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
|
||||
>
|
||||
<Breadcrumbs :items="breadbrumbs" />
|
||||
<Button variant="solid">
|
||||
{{ __('Save') }}
|
||||
</Button>
|
||||
</header>
|
||||
<div v-if="program.doc" class="w-1/2 mx-auto pt-10 space-y-5">
|
||||
<FormControl :label="__('Title')" v-model="program.doc.title" />
|
||||
<div>
|
||||
<div>
|
||||
<div class="font-semibold">
|
||||
{{ __('Program Courses') }}
|
||||
</div>
|
||||
<div></div>
|
||||
</div>
|
||||
<ListView
|
||||
v-if="program.doc.program_courses.length"
|
||||
:columns="courseColumns"
|
||||
:rows="program.doc.program_courses"
|
||||
row-key="name"
|
||||
:options="{ showTooltip: false, selectable: false }"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
|
||||
>
|
||||
<ListHeaderItem :item="item" v-for="item in courseColumns">
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<ListRow :row="row" />
|
||||
</ListRows>
|
||||
</ListView>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
createDocumentResource,
|
||||
FormControl,
|
||||
ListView,
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
} from 'frappe-ui'
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
programName: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const program = createDocumentResource({
|
||||
doctype: 'LMS Program',
|
||||
name: props.programName,
|
||||
auto: true,
|
||||
cache: ['program', props.programName],
|
||||
})
|
||||
|
||||
console.log(program)
|
||||
|
||||
const courseColumns = computed(() => [
|
||||
{
|
||||
label: 'Course',
|
||||
key: 'course_title',
|
||||
width: '2',
|
||||
},
|
||||
])
|
||||
|
||||
const breadbrumbs = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: 'Programs',
|
||||
to: { name: 'Programs' },
|
||||
},
|
||||
{
|
||||
label: props.programName === 'new' ? 'New Program' : props.programName,
|
||||
},
|
||||
]
|
||||
})
|
||||
</script>
|
||||
164
frontend/src/pages/Programs.vue
Normal file
164
frontend/src/pages/Programs.vue
Normal file
@@ -0,0 +1,164 @@
|
||||
<template>
|
||||
<header
|
||||
class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-white px-3 py-2.5 sm:px-5"
|
||||
>
|
||||
<Breadcrumbs :items="breadbrumbs" />
|
||||
<Button variant="solid" @click="showDialog = true">
|
||||
<template #prefix>
|
||||
<Plus class="w-4 h-4" />
|
||||
</template>
|
||||
{{ __('New Program') }}
|
||||
</Button>
|
||||
</header>
|
||||
<div class="pt-5 px-5">
|
||||
<div v-if="programs.data?.length">
|
||||
<ListView
|
||||
:columns="programColumns"
|
||||
:rows="programs.data"
|
||||
row-key="name"
|
||||
:options="{ showTooltip: false, selectable: false }"
|
||||
>
|
||||
<ListHeader
|
||||
class="mb-2 grid items-center space-x-4 rounded bg-gray-100 p-2"
|
||||
>
|
||||
<ListHeaderItem :item="item" v-for="item in programColumns">
|
||||
</ListHeaderItem>
|
||||
</ListHeader>
|
||||
<ListRows>
|
||||
<router-link
|
||||
v-for="row in programs.data"
|
||||
:to="{
|
||||
name: 'ProgramForm',
|
||||
params: {
|
||||
programName: row.name,
|
||||
},
|
||||
}"
|
||||
>
|
||||
<ListRow :row="row" />
|
||||
</router-link>
|
||||
</ListRows>
|
||||
</ListView>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-center p-5 text-gray-600 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
||||
>
|
||||
<Route class="size-10 mx-auto stroke-1 text-gray-500" />
|
||||
<div class="text-xl font-medium">
|
||||
{{ __('No programs found') }}
|
||||
</div>
|
||||
<div class="leading-5">
|
||||
{{
|
||||
__(
|
||||
'Program lets you create learning paths and assign them to your students. To create one, click on the "New Program" button above.'
|
||||
)
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Dialog
|
||||
v-model="showDialog"
|
||||
:options="{
|
||||
title: __('New Program'),
|
||||
actions: [
|
||||
{
|
||||
label: __('Create'),
|
||||
variant: 'solid',
|
||||
onClick: () => createProgram(close),
|
||||
},
|
||||
],
|
||||
}"
|
||||
>
|
||||
<template #body-content>
|
||||
<FormControl :label="__('Title')" v-model="title" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
createListResource,
|
||||
Dialog,
|
||||
FormControl,
|
||||
ListView,
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { Plus, Route } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const user = inject('$user')
|
||||
const router = useRouter()
|
||||
const showDialog = ref(false)
|
||||
const title = ref('')
|
||||
|
||||
onMounted(() => {
|
||||
if (!user.data?.is_moderator) {
|
||||
router.push({ name: 'Courses' })
|
||||
}
|
||||
})
|
||||
|
||||
const programs = createListResource({
|
||||
doctype: 'LMS Program',
|
||||
fields: ['title', 'name', 'program_courses'],
|
||||
auto: true,
|
||||
cache: 'programs',
|
||||
transform(data) {
|
||||
return data.map((program) => {
|
||||
console.log(program)
|
||||
program.program_courses = program.program_courses?.length
|
||||
return program
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
const createProgram = async (close) => {
|
||||
programs.insert.submit(
|
||||
{
|
||||
title: title.value,
|
||||
},
|
||||
{
|
||||
onSuccess(data) {
|
||||
showDialog.value = false
|
||||
router.push({ name: 'ProgramForm', params: { programName: data.name } })
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
const breadbrumbs = computed(() => [
|
||||
{
|
||||
label: 'Programs',
|
||||
route: {
|
||||
name: 'Programs',
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
const programColumns = computed(() => {
|
||||
return [
|
||||
{
|
||||
label: __('Title'),
|
||||
key: 'title',
|
||||
width: 2,
|
||||
},
|
||||
{
|
||||
label: __('Courses'),
|
||||
key: 'program_courses',
|
||||
width: 1,
|
||||
align: 'center',
|
||||
},
|
||||
{
|
||||
label: __('Members'),
|
||||
key: 'program_members',
|
||||
width: 1,
|
||||
align: 'center',
|
||||
},
|
||||
]
|
||||
})
|
||||
</script>
|
||||
@@ -182,6 +182,17 @@ const routes = [
|
||||
component: () => import('@/pages/QuizSubmission.vue'),
|
||||
props: true,
|
||||
},
|
||||
{
|
||||
path: '/programs',
|
||||
name: 'Programs',
|
||||
component: () => import('@/pages/Programs.vue'),
|
||||
},
|
||||
{
|
||||
path: '/programs/:programName',
|
||||
name: 'ProgramForm',
|
||||
component: () => import('@/pages/ProgramForm.vue'),
|
||||
props: true,
|
||||
},
|
||||
]
|
||||
|
||||
let router = createRouter({
|
||||
|
||||
0
lms/lms/doctype/lms_program/__init__.py
Normal file
0
lms/lms/doctype/lms_program/__init__.py
Normal file
8
lms/lms/doctype/lms_program/lms_program.js
Normal file
8
lms/lms/doctype/lms_program/lms_program.js
Normal file
@@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2024, Frappe and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
// frappe.ui.form.on("LMS Program", {
|
||||
// refresh(frm) {
|
||||
|
||||
// },
|
||||
// });
|
||||
60
lms/lms/doctype/lms_program/lms_program.json
Normal file
60
lms/lms/doctype/lms_program/lms_program.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:title",
|
||||
"creation": "2024-11-18 12:27:13.283169",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"title",
|
||||
"program_courses",
|
||||
"program_members"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "program_courses",
|
||||
"fieldtype": "Table",
|
||||
"label": "Program Courses",
|
||||
"options": "LMS Program Course"
|
||||
},
|
||||
{
|
||||
"fieldname": "program_members",
|
||||
"fieldtype": "Table",
|
||||
"label": "Program Members",
|
||||
"options": "LMS Program Member"
|
||||
},
|
||||
{
|
||||
"fieldname": "title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Title",
|
||||
"reqd": 1,
|
||||
"unique": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-18 14:08:26.958831",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Program",
|
||||
"naming_rule": "By fieldname",
|
||||
"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": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
9
lms/lms/doctype/lms_program/lms_program.py
Normal file
9
lms/lms/doctype/lms_program/lms_program.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSProgram(Document):
|
||||
pass
|
||||
21
lms/lms/doctype/lms_program/test_lms_program.py
Normal file
21
lms/lms/doctype/lms_program/test_lms_program.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# Copyright (c) 2024, Frappe and Contributors
|
||||
# See license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
|
||||
# On IntegrationTestCase, the doctype test records and all
|
||||
# link-field test record depdendencies are recursively loaded
|
||||
# Use these module variables to add/remove to/from that list
|
||||
EXTRA_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
IGNORE_TEST_RECORD_DEPENDENCIES = [] # eg. ["User"]
|
||||
|
||||
|
||||
class TestLMSProgram(UnitTestCase):
|
||||
"""
|
||||
Unit tests for LMSProgram.
|
||||
Use this class for testing individual functions and methods.
|
||||
"""
|
||||
|
||||
pass
|
||||
0
lms/lms/doctype/lms_program_course/__init__.py
Normal file
0
lms/lms/doctype/lms_program_course/__init__.py
Normal file
42
lms/lms/doctype/lms_program_course/lms_program_course.json
Normal file
42
lms/lms/doctype/lms_program_course/lms_program_course.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-11-18 12:27:37.030302",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"course",
|
||||
"course_title"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Course",
|
||||
"options": "LMS Course",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "course.title",
|
||||
"fieldname": "course_title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Course Title",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-18 12:43:46.800199",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Program Course",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
9
lms/lms/doctype/lms_program_course/lms_program_course.py
Normal file
9
lms/lms/doctype/lms_program_course/lms_program_course.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSProgramCourse(Document):
|
||||
pass
|
||||
0
lms/lms/doctype/lms_program_member/__init__.py
Normal file
0
lms/lms/doctype/lms_program_member/__init__.py
Normal file
42
lms/lms/doctype/lms_program_member/lms_program_member.json
Normal file
42
lms/lms/doctype/lms_program_member/lms_program_member.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2024-11-18 12:29:13.615014",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"member",
|
||||
"full_name"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "member",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Member",
|
||||
"options": "User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "member.full_name",
|
||||
"fieldname": "full_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Full Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-18 12:44:02.648786",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Program Member",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "creation",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
||||
9
lms/lms/doctype/lms_program_member/lms_program_member.py
Normal file
9
lms/lms/doctype/lms_program_member/lms_program_member.py
Normal file
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2024, Frappe and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class LMSProgramMember(Document):
|
||||
pass
|
||||
@@ -5,13 +5,15 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"general_tab",
|
||||
"default_home",
|
||||
"send_calendar_invite_for_evaluations",
|
||||
"is_onboarding_complete",
|
||||
"column_break_zdel",
|
||||
"enable_learning_paths",
|
||||
"unsplash_access_key",
|
||||
"livecode_url",
|
||||
"section_break_szgq",
|
||||
"send_calendar_invite_for_evaluations",
|
||||
"show_day_view",
|
||||
"column_break_2",
|
||||
"show_dashboard",
|
||||
@@ -80,6 +82,7 @@
|
||||
{
|
||||
"fieldname": "mentor_request_section",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1,
|
||||
"label": "Mentor Request"
|
||||
},
|
||||
{
|
||||
@@ -127,6 +130,7 @@
|
||||
{
|
||||
"fieldname": "section_break_szgq",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 1,
|
||||
"label": "Batch Settings"
|
||||
},
|
||||
{
|
||||
@@ -336,12 +340,23 @@
|
||||
"fieldname": "payments_app_is_not_installed",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Payments app is not installed"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_learning_paths",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Learning Paths"
|
||||
},
|
||||
{
|
||||
"fieldname": "general_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "General"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-10-01 12:15:49.800242",
|
||||
"modified": "2024-11-18 12:52:41.236252",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Settings",
|
||||
|
||||
Reference in New Issue
Block a user