3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,4 +11,5 @@ __pycache__/
|
||||
node_modules
|
||||
package-lock.json
|
||||
lms/public/frontend
|
||||
lms/www/lms.html
|
||||
lms/www/lms.html
|
||||
frappe-ui
|
||||
@@ -35,7 +35,6 @@ bench new-site lms.localhost \
|
||||
bench --site lms.localhost install-app lms
|
||||
bench --site lms.localhost set-config developer_mode 1
|
||||
bench --site lms.localhost clear-cache
|
||||
bench --site lms.localhost set-config mute_emails 1
|
||||
bench use lms.localhost
|
||||
|
||||
bench start
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
class="flex flex-col overflow-hidden"
|
||||
:class="isSidebarCollapsed ? 'items-center' : ''"
|
||||
>
|
||||
<UserDropdown class="p-2" :isCollapsed="isSidebarCollapsed" />
|
||||
<UserDropdown :isCollapsed="isSidebarCollapsed" />
|
||||
<div class="flex flex-col" v-if="sidebarSettings.data">
|
||||
<SidebarLink
|
||||
v-for="link in sidebarLinks"
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
<script setup>
|
||||
import { Star } from 'lucide-vue-next'
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
|
||||
270
frontend/src/components/Modals/Settings.vue
Normal file
270
frontend/src/components/Modals/Settings.vue
Normal file
@@ -0,0 +1,270 @@
|
||||
<template>
|
||||
<Dialog v-model="show" :options="{ size: '6xl' }">
|
||||
<template #body>
|
||||
<div class="flex h-[calc(100vh_-_8rem)]">
|
||||
<div class="flex w-52 shrink-0 flex-col bg-gray-50 p-2">
|
||||
<h1 class="mb-3 px-2 pt-2 text-lg font-semibold">
|
||||
{{ __('Settings') }}
|
||||
</h1>
|
||||
<div v-for="tab in tabs">
|
||||
<div
|
||||
v-if="!tab.hideLabel"
|
||||
class="mb-2 mt-3 flex cursor-pointer gap-1.5 px-1 text-base font-medium text-gray-600 transition-all duration-300 ease-in-out"
|
||||
>
|
||||
<span>{{ __(tab.label) }}</span>
|
||||
</div>
|
||||
<nav class="space-y-1">
|
||||
<SidebarLink
|
||||
v-for="item in tab.items"
|
||||
:link="item"
|
||||
class="w-full"
|
||||
:class="
|
||||
activeTab?.label == item.label
|
||||
? 'bg-white shadow-sm'
|
||||
: 'hover:bg-gray-100'
|
||||
"
|
||||
@click="activeTab = item"
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-1 flex-col overflow-y-auto">
|
||||
<SettingDetails
|
||||
v-if="activeTab && data.doc"
|
||||
:fields="activeTab.fields"
|
||||
:data="data"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Dialog, createDocumentResource } from 'frappe-ui'
|
||||
import { ref, computed, watch } from 'vue'
|
||||
import SettingDetails from '../SettingDetails.vue'
|
||||
import SidebarLink from '@/components/SidebarLink.vue'
|
||||
|
||||
const show = defineModel()
|
||||
const doctype = ref('LMS Settings')
|
||||
const activeTab = ref(null)
|
||||
|
||||
const data = createDocumentResource({
|
||||
doctype: doctype.value,
|
||||
name: doctype.value,
|
||||
fields: ['*'],
|
||||
cache: doctype.value,
|
||||
auto: true,
|
||||
})
|
||||
|
||||
const tabs = computed(() => {
|
||||
let _tabs = [
|
||||
{
|
||||
label: 'Settings',
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Payment Gateway',
|
||||
icon: 'DollarSign',
|
||||
fields: [
|
||||
{
|
||||
label: 'Razorpay Key',
|
||||
name: 'razorpay_key',
|
||||
type: 'text',
|
||||
},
|
||||
{
|
||||
label: 'Razorpay Secret',
|
||||
name: 'razorpay_secret',
|
||||
type: 'password',
|
||||
},
|
||||
{
|
||||
label: 'Default Currency',
|
||||
name: 'default_currency',
|
||||
type: 'Link',
|
||||
doctype: 'Currency',
|
||||
},
|
||||
{
|
||||
type: 'Column Break',
|
||||
},
|
||||
{
|
||||
label: 'Apply GST for India',
|
||||
name: 'apply_gst',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Show USD equivalent amount',
|
||||
name: 'show_usd_equivalent',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Apply rounding on equivalent amount',
|
||||
name: 'apply_rounding',
|
||||
type: 'checkbox',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Signup',
|
||||
icon: 'LogIn',
|
||||
fields: [
|
||||
{
|
||||
label: 'Show terms of use on signup page',
|
||||
name: 'terms_of_use',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Terms of Use Page',
|
||||
name: 'terms_page',
|
||||
type: 'Link',
|
||||
doctype: 'Web Page',
|
||||
},
|
||||
{
|
||||
label: 'Ask user category during signup',
|
||||
name: 'user_category',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
type: 'Column Break',
|
||||
},
|
||||
{
|
||||
label: 'Show privacy policy on signup page',
|
||||
name: 'privacy_policy',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Privacy Policy Page',
|
||||
name: 'privacy_policy_page',
|
||||
type: 'Link',
|
||||
doctype: 'Web Page',
|
||||
},
|
||||
{
|
||||
type: 'Column Break',
|
||||
},
|
||||
{
|
||||
label: 'Show cookie policy on signup page',
|
||||
name: 'cookie_policy',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Cookie Policy Page',
|
||||
name: 'cookie_policy_page',
|
||||
type: 'Link',
|
||||
doctype: 'Web Page',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Sidebar',
|
||||
icon: 'PanelLeftIcon',
|
||||
fields: [
|
||||
{
|
||||
label: 'Courses',
|
||||
name: 'courses',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Batches',
|
||||
name: 'batches',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Certified Participants',
|
||||
name: 'certified_participants',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
type: 'Column Break',
|
||||
},
|
||||
{
|
||||
label: 'Jobs',
|
||||
name: 'jobs',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Statistics',
|
||||
name: 'statistics',
|
||||
type: 'checkbox',
|
||||
},
|
||||
{
|
||||
label: 'Notifications',
|
||||
name: 'notifications',
|
||||
type: 'checkbox',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Email Templates',
|
||||
icon: 'MailPlus',
|
||||
fields: [
|
||||
{
|
||||
label: 'Batch Confirmation Template',
|
||||
name: 'batch_confirmation_template',
|
||||
doctype: 'Email Template',
|
||||
type: 'Link',
|
||||
},
|
||||
{
|
||||
label: 'Certification Template',
|
||||
name: 'certification_template',
|
||||
doctype: 'Email Template',
|
||||
type: 'Link',
|
||||
},
|
||||
{
|
||||
label: 'Assignment Submission Template',
|
||||
name: 'assignment_submission_template',
|
||||
doctype: 'Email Template',
|
||||
type: 'Link',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
/* {
|
||||
label: 'Settings',
|
||||
hideLabel: true,
|
||||
items: [
|
||||
{
|
||||
label: 'Members',
|
||||
icon: "UserRoundPlus",
|
||||
component: markRaw(MemberSettings),
|
||||
},
|
||||
],
|
||||
}, */
|
||||
]
|
||||
|
||||
return _tabs.map((tab) => {
|
||||
tab.items = tab.items.filter((item) => {
|
||||
if (item.condition) {
|
||||
return item.condition()
|
||||
}
|
||||
return true
|
||||
})
|
||||
return tab
|
||||
})
|
||||
})
|
||||
|
||||
watch(show, () => {
|
||||
if (show.value) {
|
||||
activeTab.value = tabs.value[0].items[0]
|
||||
} else {
|
||||
activeTab.value = null
|
||||
}
|
||||
})
|
||||
</script>
|
||||
89
frontend/src/components/SettingDetails.vue
Normal file
89
frontend/src/components/SettingDetails.vue
Normal file
@@ -0,0 +1,89 @@
|
||||
<template>
|
||||
<div class="flex flex-col justify-between h-full p-8">
|
||||
<div class="flex space-x-10">
|
||||
<div v-for="(column, index) in columns" :key="index">
|
||||
<div class="flex flex-col space-y-4">
|
||||
<div v-for="field in column" :class="width">
|
||||
<Link
|
||||
v-if="field.type == 'Link'"
|
||||
v-model="field.value"
|
||||
:doctype="field.doctype"
|
||||
:label="field.label"
|
||||
/>
|
||||
<FormControl
|
||||
v-else
|
||||
:key="field.name"
|
||||
v-model="field.value"
|
||||
:label="field.label"
|
||||
:type="field.type"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-row-reverse mt-auto">
|
||||
<Button variant="solid" :loading="data.save.loading" @click="update">
|
||||
{{ __('Update') }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { FormControl, Button } from 'frappe-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
let width = ref('w-full')
|
||||
|
||||
const props = defineProps({
|
||||
fields: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
data: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const columns = computed(() => {
|
||||
const cols = []
|
||||
let currentColumn = []
|
||||
|
||||
props.fields.forEach((field) => {
|
||||
if (field.type === 'Column Break') {
|
||||
if (currentColumn.length > 0) {
|
||||
cols.push(currentColumn)
|
||||
currentColumn = []
|
||||
}
|
||||
} else {
|
||||
if (field.type == 'checkbox') {
|
||||
field.value = props.data.doc[field.name] ? true : false
|
||||
} else {
|
||||
field.value = props.data.doc[field.name]
|
||||
}
|
||||
currentColumn.push(field)
|
||||
}
|
||||
})
|
||||
|
||||
if (currentColumn.length > 0) {
|
||||
cols.push(currentColumn)
|
||||
}
|
||||
|
||||
if (cols.length == 3) {
|
||||
width.value = 'w-64'
|
||||
} else {
|
||||
width.value = 'w-96'
|
||||
}
|
||||
|
||||
return cols
|
||||
})
|
||||
|
||||
const update = () => {
|
||||
props.fields.forEach((f) => {
|
||||
props.data.doc[f.name] = f.value
|
||||
})
|
||||
props.data.save.submit()
|
||||
}
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Dropdown :options="userDropdownOptions">
|
||||
<Dropdown class="p-2" :options="userDropdownOptions">
|
||||
<template v-slot="{ open }">
|
||||
<button
|
||||
class="flex h-12 py-2 items-center rounded-md duration-300 ease-in-out"
|
||||
@@ -56,6 +56,10 @@
|
||||
</button>
|
||||
</template>
|
||||
</Dropdown>
|
||||
<SettingsModal
|
||||
v-if="userResource.data?.is_moderator"
|
||||
v-model="showSettingsModal"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
@@ -68,12 +72,16 @@ import {
|
||||
LogOut,
|
||||
User,
|
||||
ArrowRightLeft,
|
||||
Settings,
|
||||
} from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { convertToTitleCase } from '../utils'
|
||||
import { usersStore } from '@/stores/user'
|
||||
import { ref } from 'vue'
|
||||
import SettingsModal from '@/components/Modals/Settings.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const showSettingsModal = ref(false)
|
||||
const { logout, branding } = sessionStore()
|
||||
let { userResource } = usersStore()
|
||||
let { isLoggedIn } = sessionStore()
|
||||
@@ -109,6 +117,16 @@ const userDropdownOptions = [
|
||||
else return false
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Settings,
|
||||
label: 'Settings',
|
||||
onClick: () => {
|
||||
showSettingsModal.value = true
|
||||
},
|
||||
condition: () => {
|
||||
return userResource.data?.is_moderator
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: LogOut,
|
||||
label: 'Log out',
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
@input="courses.reload()"
|
||||
>
|
||||
<template #prefix>
|
||||
<Search class="w-4 h-4 stroke-1.5" name="search" />
|
||||
<Search class="w-4 h-4 stroke-1.5 text-gray-600" name="search" />
|
||||
</template>
|
||||
</FormControl>
|
||||
</div>
|
||||
|
||||
@@ -266,7 +266,9 @@ def get_chart_details():
|
||||
"upcoming": 0,
|
||||
},
|
||||
)
|
||||
details.users = frappe.db.count("User", {"enabled": 1})
|
||||
details.users = frappe.db.count(
|
||||
"User", {"enabled": 1, "name": ["not in", ("Administrator", "Guest")]}
|
||||
)
|
||||
details.completions = frappe.db.count(
|
||||
"LMS Enrollment", {"progress": ["like", "%100%"]}
|
||||
)
|
||||
|
||||
@@ -10,14 +10,9 @@
|
||||
"column_break_zdel",
|
||||
"unsplash_access_key",
|
||||
"livecode_url",
|
||||
"course_settings_section",
|
||||
"search_placeholder",
|
||||
"column_break_iqxy",
|
||||
"portal_course_creation",
|
||||
"section_break_szgq",
|
||||
"send_calendar_invite_for_evaluations",
|
||||
"show_day_view",
|
||||
"allow_student_progress",
|
||||
"column_break_2",
|
||||
"show_dashboard",
|
||||
"show_courses",
|
||||
@@ -48,7 +43,6 @@
|
||||
"notifications",
|
||||
"section_break_qlss",
|
||||
"sidebar_items",
|
||||
"mentor_request_tab",
|
||||
"mentor_request_section",
|
||||
"mentor_request_creation",
|
||||
"mentor_request_status_update",
|
||||
@@ -98,11 +92,6 @@
|
||||
"fieldtype": "Column Break",
|
||||
"label": "Show Tab in Batch"
|
||||
},
|
||||
{
|
||||
"fieldname": "search_placeholder",
|
||||
"fieldtype": "Data",
|
||||
"label": "Course List Search Bar Placeholder"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "terms_of_use",
|
||||
@@ -139,13 +128,6 @@
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "Course Creator Role",
|
||||
"fieldname": "portal_course_creation",
|
||||
"fieldtype": "Select",
|
||||
"label": "Course Creation Access Through Website To",
|
||||
"options": "Course Creator Role\nAnyone"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -203,19 +185,6 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Signup Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "mentor_request_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"hidden": 1,
|
||||
"label": "Mentor Request"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "allow_student_progress",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Allow students to see each others progress in class"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_section",
|
||||
"fieldtype": "Section Break"
|
||||
@@ -230,15 +199,6 @@
|
||||
"fieldname": "column_break_cfcv",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "course_settings_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Course Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_iqxy",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "razorpay_key",
|
||||
"fieldtype": "Data",
|
||||
@@ -423,7 +383,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2024-06-27 21:57:02.193336",
|
||||
"modified": "2024-08-13 19:02:58.714080",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Settings",
|
||||
|
||||
@@ -517,13 +517,6 @@ def can_create_courses(course, member=None):
|
||||
if has_course_instructor_role(member) and member in instructors:
|
||||
return True
|
||||
|
||||
portal_course_creation = frappe.db.get_single_value(
|
||||
"LMS Settings", "portal_course_creation"
|
||||
)
|
||||
|
||||
if portal_course_creation == "Anyone" and member in instructors:
|
||||
return True
|
||||
|
||||
if not course and has_course_instructor_role(member):
|
||||
return True
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
{% set search_placeholder = frappe.db.get_single_value("LMS Settings", "search_placeholder") %}
|
||||
{% set portal_course_creation = frappe.db.get_single_value("LMS Settings", "portal_course_creation") %}
|
||||
|
||||
|
||||
<div class="modal fade search-modal" id="search-modal" tabindex="-1" role="dialog">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-body">
|
||||
<input class="search search-course" id="search-course" placeholder="{{ _(search_placeholder) or 'Search for courses' }}">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script> {% include "lms/templates/search_course/search_course.js" %} </script>
|
||||
@@ -1,72 +0,0 @@
|
||||
frappe.ready(() => {
|
||||
$("#search-course").keyup((e) => {
|
||||
search_course(e);
|
||||
});
|
||||
|
||||
$("#open-search").click((e) => {
|
||||
show_search_bar(e);
|
||||
});
|
||||
|
||||
$("#search-modal").on("hidden.bs.modal", () => {
|
||||
hide_search_bar();
|
||||
});
|
||||
|
||||
$(document).keydown(function (e) {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key == "k") {
|
||||
show_search_bar(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const search_course = (e) => {
|
||||
let input = $(e.currentTarget).val();
|
||||
if (input == window.input) return;
|
||||
window.input = input;
|
||||
|
||||
if (input.length < 3 || input.trim() == "") {
|
||||
$(".result-row").remove();
|
||||
return;
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: "lms.lms.doctype.lms_course.lms_course.search_course",
|
||||
args: {
|
||||
text: input,
|
||||
},
|
||||
callback: (data) => {
|
||||
render_course_list(data);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const render_course_list = (data) => {
|
||||
let courses = data.message;
|
||||
$(".result-row").remove();
|
||||
|
||||
if (!courses.length) {
|
||||
let element = `<a class="result-row">
|
||||
${__("No result found")}
|
||||
</a>`;
|
||||
$(element).insertAfter("#search-course");
|
||||
return;
|
||||
}
|
||||
|
||||
for (let i in courses) {
|
||||
let element = `<a class="result-row" href="/courses/${courses[i].name}">
|
||||
${courses[i].title}
|
||||
</a>`;
|
||||
$(element).insertAfter("#search-course");
|
||||
}
|
||||
};
|
||||
|
||||
const show_search_bar = (e) => {
|
||||
$("#search-modal").modal("show");
|
||||
setTimeout(() => {
|
||||
$("#search-course").focus();
|
||||
}, 1000);
|
||||
};
|
||||
|
||||
const hide_search_bar = (e) => {
|
||||
$("#search-course").val("");
|
||||
$(".result-row").remove();
|
||||
};
|
||||
Reference in New Issue
Block a user