chore: fixed merge conflicts
This commit is contained in:
4
.github/workflows/ci.yml
vendored
4
.github/workflows/ci.yml
vendored
@@ -34,9 +34,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
python-version: '3.10'
|
python-version: '3.10'
|
||||||
- name: setup node
|
- name: setup node
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '18'
|
node-version: '20'
|
||||||
check-latest: true
|
check-latest: true
|
||||||
- name: setup cache for bench
|
- name: setup cache for bench
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
|
|||||||
4
.github/workflows/ui-tests.yml
vendored
4
.github/workflows/ui-tests.yml
vendored
@@ -48,9 +48,9 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18
|
node-version: 20
|
||||||
check-latest: true
|
check-latest: true
|
||||||
|
|
||||||
- name: Add to Hosts
|
- name: Add to Hosts
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ describe("Batch Creation", () => {
|
|||||||
|
|
||||||
// Create a batch
|
// Create a batch
|
||||||
cy.get("button").contains("Create").click();
|
cy.get("button").contains("Create").click();
|
||||||
|
cy.get("span").contains("New Batch").click();
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.url().should("include", "/batches/new/edit");
|
cy.url().should("include", "/batches/new/edit");
|
||||||
cy.get("label").contains("Title").type("Test Batch");
|
cy.get("label").contains("Title").type("Test Batch");
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ describe("Course Creation", () => {
|
|||||||
|
|
||||||
// Create a course
|
// Create a course
|
||||||
cy.get("button").contains("Create").click();
|
cy.get("button").contains("Create").click();
|
||||||
|
cy.get("span").contains("New Course").click();
|
||||||
cy.wait(500);
|
cy.wait(500);
|
||||||
cy.url().should("include", "/courses/new/edit");
|
cy.url().should("include", "/courses/new/edit");
|
||||||
|
|
||||||
@@ -55,6 +56,7 @@ describe("Course Creation", () => {
|
|||||||
.parent()
|
.parent()
|
||||||
.within(() => {
|
.within(() => {
|
||||||
cy.get("input").click().type("frappe");
|
cy.get("input").click().type("frappe");
|
||||||
|
cy.wait(500);
|
||||||
cy.get("input")
|
cy.get("input")
|
||||||
.invoke("attr", "aria-controls")
|
.invoke("attr", "aria-controls")
|
||||||
.as("instructor_list_id");
|
.as("instructor_list_id");
|
||||||
|
|||||||
Submodule frappe-ui updated: 204333c925...78025c6794
9
frontend/components.d.ts
vendored
9
frontend/components.d.ts
vendored
@@ -11,8 +11,8 @@ declare module 'vue' {
|
|||||||
AdminBatchDashboard: typeof import('./src/components/AdminBatchDashboard.vue')['default']
|
AdminBatchDashboard: typeof import('./src/components/AdminBatchDashboard.vue')['default']
|
||||||
Annoucements: typeof import('./src/components/Annoucements.vue')['default']
|
Annoucements: typeof import('./src/components/Annoucements.vue')['default']
|
||||||
AnnouncementModal: typeof import('./src/components/Modals/AnnouncementModal.vue')['default']
|
AnnouncementModal: typeof import('./src/components/Modals/AnnouncementModal.vue')['default']
|
||||||
Apps: typeof import('./src/components/Apps.vue')['default']
|
Apps: typeof import('./src/components/Sidebar/Apps.vue')['default']
|
||||||
AppSidebar: typeof import('./src/components/AppSidebar.vue')['default']
|
AppSidebar: typeof import('./src/components/Sidebar/AppSidebar.vue')['default']
|
||||||
AssessmentModal: typeof import('./src/components/Modals/AssessmentModal.vue')['default']
|
AssessmentModal: typeof import('./src/components/Modals/AssessmentModal.vue')['default']
|
||||||
AssessmentPlugin: typeof import('./src/components/AssessmentPlugin.vue')['default']
|
AssessmentPlugin: typeof import('./src/components/AssessmentPlugin.vue')['default']
|
||||||
Assessments: typeof import('./src/components/Assessments.vue')['default']
|
Assessments: typeof import('./src/components/Assessments.vue')['default']
|
||||||
@@ -44,6 +44,7 @@ declare module 'vue' {
|
|||||||
ColorSwatches: typeof import('./src/components/Controls/ColorSwatches.vue')['default']
|
ColorSwatches: typeof import('./src/components/Controls/ColorSwatches.vue')['default']
|
||||||
CommandPalette: typeof import('./src/components/CommandPalette/CommandPalette.vue')['default']
|
CommandPalette: typeof import('./src/components/CommandPalette/CommandPalette.vue')['default']
|
||||||
CommandPaletteGroup: typeof import('./src/components/CommandPalette/CommandPaletteGroup.vue')['default']
|
CommandPaletteGroup: typeof import('./src/components/CommandPalette/CommandPaletteGroup.vue')['default']
|
||||||
|
Configuration: typeof import('./src/components/Sidebar/Configuration.vue')['default']
|
||||||
ContactUsEmail: typeof import('./src/components/ContactUsEmail.vue')['default']
|
ContactUsEmail: typeof import('./src/components/ContactUsEmail.vue')['default']
|
||||||
CouponDetails: typeof import('./src/components/Settings/Coupons/CouponDetails.vue')['default']
|
CouponDetails: typeof import('./src/components/Settings/Coupons/CouponDetails.vue')['default']
|
||||||
CouponItems: typeof import('./src/components/Settings/Coupons/CouponItems.vue')['default']
|
CouponItems: typeof import('./src/components/Settings/Coupons/CouponItems.vue')['default']
|
||||||
@@ -113,7 +114,7 @@ declare module 'vue' {
|
|||||||
SettingDetails: typeof import('./src/components/Settings/SettingDetails.vue')['default']
|
SettingDetails: typeof import('./src/components/Settings/SettingDetails.vue')['default']
|
||||||
SettingFields: typeof import('./src/components/Settings/SettingFields.vue')['default']
|
SettingFields: typeof import('./src/components/Settings/SettingFields.vue')['default']
|
||||||
Settings: typeof import('./src/components/Settings/Settings.vue')['default']
|
Settings: typeof import('./src/components/Settings/Settings.vue')['default']
|
||||||
SidebarLink: typeof import('./src/components/SidebarLink.vue')['default']
|
SidebarLink: typeof import('./src/components/Sidebar/SidebarLink.vue')['default']
|
||||||
StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default']
|
StudentHeatmap: typeof import('./src/components/StudentHeatmap.vue')['default']
|
||||||
StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default']
|
StudentModal: typeof import('./src/components/Modals/StudentModal.vue')['default']
|
||||||
Tags: typeof import('./src/components/Tags.vue')['default']
|
Tags: typeof import('./src/components/Tags.vue')['default']
|
||||||
@@ -125,7 +126,7 @@ declare module 'vue' {
|
|||||||
Uploader: typeof import('./src/components/Controls/Uploader.vue')['default']
|
Uploader: typeof import('./src/components/Controls/Uploader.vue')['default']
|
||||||
UploadPlugin: typeof import('./src/components/UploadPlugin.vue')['default']
|
UploadPlugin: typeof import('./src/components/UploadPlugin.vue')['default']
|
||||||
UserAvatar: typeof import('./src/components/UserAvatar.vue')['default']
|
UserAvatar: typeof import('./src/components/UserAvatar.vue')['default']
|
||||||
UserDropdown: typeof import('./src/components/UserDropdown.vue')['default']
|
UserDropdown: typeof import('./src/components/Sidebar/UserDropdown.vue')['default']
|
||||||
VideoBlock: typeof import('./src/components/VideoBlock.vue')['default']
|
VideoBlock: typeof import('./src/components/VideoBlock.vue')['default']
|
||||||
VideoStatistics: typeof import('./src/components/Modals/VideoStatistics.vue')['default']
|
VideoStatistics: typeof import('./src/components/Modals/VideoStatistics.vue')['default']
|
||||||
ZoomAccountModal: typeof import('./src/components/Modals/ZoomAccountModal.vue')['default']
|
ZoomAccountModal: typeof import('./src/components/Modals/ZoomAccountModal.vue')['default']
|
||||||
|
|||||||
@@ -6,56 +6,60 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"build": "vite build --base=/assets/lms/frontend/ && yarn copy-html-entry",
|
"build": "vite build --base=/assets/lms/frontend/ && yarn copy-html-entry && yarn copy-colors-json",
|
||||||
"copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/lms.html"
|
"copy-html-entry": "cp ../lms/public/frontend/index.html ../lms/www/lms.html",
|
||||||
|
"copy-colors-json": "cp node_modules/frappe-ui/src/tailwind/colors.json src/utils/frappe-ui-colors.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/lang-html": "^6.4.9",
|
"@codemirror/lang-html": "6.4.9",
|
||||||
"@codemirror/lang-javascript": "^6.2.4",
|
"@codemirror/lang-javascript": "6.2.4",
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "6.0.1",
|
||||||
"@codemirror/lang-python": "^6.2.1",
|
"@codemirror/lang-python": "6.2.1",
|
||||||
"@editorjs/checklist": "^1.6.0",
|
"@editorjs/checklist": "1.6.0",
|
||||||
"@editorjs/code": "^2.9.0",
|
"@editorjs/code": "2.9.0",
|
||||||
"@editorjs/editorjs": "^2.29.0",
|
"@editorjs/editorjs": "2.29.0",
|
||||||
"@editorjs/embed": "^2.7.0",
|
"@editorjs/embed": "2.7.0",
|
||||||
"@editorjs/header": "^2.8.1",
|
"@editorjs/header": "2.8.1",
|
||||||
"@editorjs/inline-code": "^1.5.0",
|
"@editorjs/inline-code": "1.5.0",
|
||||||
"@editorjs/nested-list": "^1.4.2",
|
"@editorjs/nested-list": "1.4.2",
|
||||||
"@editorjs/paragraph": "^2.11.3",
|
"@editorjs/paragraph": "2.11.3",
|
||||||
"@editorjs/simple-image": "^1.6.0",
|
"@editorjs/simple-image": "1.6.0",
|
||||||
"@editorjs/table": "^2.4.2",
|
"@editorjs/table": "2.4.2",
|
||||||
"@vueuse/core": "^10.4.1",
|
"@vueuse/core": "10.4.1",
|
||||||
"@vueuse/router": "^12.7.0",
|
"@vueuse/router": "12.7.0",
|
||||||
"ace-builds": "^1.36.2",
|
"ace-builds": "1.36.2",
|
||||||
"apexcharts": "^4.3.0",
|
"apexcharts": "4.3.0",
|
||||||
"chart.js": "^4.4.1",
|
"chart.js": "4.4.1",
|
||||||
"codemirror": "^6.0.1",
|
"codemirror": "6.0.1",
|
||||||
"dayjs": "^1.11.6",
|
"dayjs": "1.11.10",
|
||||||
"dompurify": "^3.2.6",
|
"dompurify": "3.2.6",
|
||||||
"feather-icons": "^4.28.0",
|
"feather-icons": "4.28.0",
|
||||||
"frappe-ui": "^0.1.214",
|
"frappe-ui": "0.1.227",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "11.11.1",
|
||||||
"lucide-vue-next": "^0.383.0",
|
"lucide-vue-next": "0.383.0",
|
||||||
"markdown-it": "^14.0.0",
|
"markdown-it": "14.0.0",
|
||||||
"pinia": "^2.0.33",
|
"pinia": "2.0.33",
|
||||||
"plyr": "^3.7.8",
|
"plyr": "3.7.8",
|
||||||
"socket.io-client": "^4.7.2",
|
"socket.io-client": "4.7.2",
|
||||||
"tailwindcss": "3.4.15",
|
"thememirror": "2.0.1",
|
||||||
"thememirror": "^2.0.1",
|
"typescript": "5.7.2",
|
||||||
"typescript": "^5.7.2",
|
"vue": "^3.5.0",
|
||||||
"vue": "^3.4.23",
|
"vue-chartjs": "5.3.0",
|
||||||
"vue-chartjs": "^5.3.0",
|
"vue-codemirror": "6.1.1",
|
||||||
"vue-codemirror": "^6.1.1",
|
"vue-draggable-next": "2.2.1",
|
||||||
"vue-draggable-next": "^2.2.1",
|
"vue-router": "4.2.2",
|
||||||
"vue-router": "^4.0.12",
|
"vue3-apexcharts": "1.8.0",
|
||||||
"vue3-apexcharts": "^1.8.0",
|
|
||||||
"vuedraggable": "4.1.0"
|
"vuedraggable": "4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^5.0.3",
|
"@vitejs/plugin-vue": "5.0.3",
|
||||||
"autoprefixer": "^10.4.2",
|
"autoprefixer": "10.4.2",
|
||||||
"postcss": "^8.4.5",
|
"postcss": "8.4.5",
|
||||||
"vite": "^5.0.11",
|
"vite": "5.0.11",
|
||||||
"vite-plugin-pwa": "^1.0.2"
|
"tailwindcss": "^3.4.15",
|
||||||
|
"vite-plugin-pwa": "0.15.0"
|
||||||
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"@iconify/utils": "2.1.7"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,6 +179,9 @@
|
|||||||
"
|
"
|
||||||
:editable="true"
|
:editable="true"
|
||||||
:fixedMenu="true"
|
:fixedMenu="true"
|
||||||
|
:uploadArgs="{
|
||||||
|
private: true,
|
||||||
|
}"
|
||||||
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
|
editorClass="prose-sm max-w-none border-b border-x bg-surface-gray-2 rounded-b-md py-1 px-2 min-h-[7rem]"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -113,7 +113,7 @@
|
|||||||
{{ __('Enroll Now') }}
|
{{ __('Enroll Now') }}
|
||||||
</Button>
|
</Button>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="isModerator"
|
v-if="canEditBatch"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'BatchForm',
|
name: 'BatchForm',
|
||||||
params: {
|
params: {
|
||||||
@@ -209,7 +209,19 @@ const isEvaluator = computed(() => {
|
|||||||
return user.data?.is_evaluator
|
return user.data?.is_evaluator
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const isInstructor = computed(() => {
|
||||||
|
return (
|
||||||
|
props.batch.data?.instructors?.filter(
|
||||||
|
(instructor) => instructor.name === user.data?.name
|
||||||
|
).length > 0
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const canAccessBatch = computed(() => {
|
const canAccessBatch = computed(() => {
|
||||||
return isModerator.value || isStudent.value || isEvaluator.value
|
return isModerator.value || isStudent.value || isEvaluator.value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const canEditBatch = computed(() => {
|
||||||
|
return isModerator.value || isInstructor.value
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -21,8 +21,10 @@
|
|||||||
:style="
|
:style="
|
||||||
modelValue
|
modelValue
|
||||||
? {
|
? {
|
||||||
backgroundColor:
|
backgroundColor: getColor(
|
||||||
theme.backgroundColor[modelValue.toLowerCase()][400],
|
modelValue.toLowerCase(),
|
||||||
|
400
|
||||||
|
),
|
||||||
}
|
}
|
||||||
: {}
|
: {}
|
||||||
"
|
"
|
||||||
@@ -55,8 +57,7 @@
|
|||||||
:key="color"
|
:key="color"
|
||||||
class="size-5 rounded-full cursor-pointer"
|
class="size-5 rounded-full cursor-pointer"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundColor:
|
backgroundColor: getColor(color.toLowerCase(), 400),
|
||||||
theme.backgroundColor[color.toLowerCase()][400],
|
|
||||||
}"
|
}"
|
||||||
@click="
|
@click="
|
||||||
(e) => {
|
(e) => {
|
||||||
@@ -79,7 +80,7 @@
|
|||||||
import { Button, FormControl, Popover } from 'frappe-ui'
|
import { Button, FormControl, Popover } from 'frappe-ui'
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { Palette, X } from 'lucide-vue-next'
|
import { Palette, X } from 'lucide-vue-next'
|
||||||
import { theme } from '@/utils/theme'
|
import { getColor } from '@/utils'
|
||||||
|
|
||||||
const emit = defineEmits(['update:modelValue', 'change'])
|
const emit = defineEmits(['update:modelValue', 'change'])
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
class="w-4 h-4 text-ink-gray-7 stroke-1.5"
|
class="w-4 h-4 text-ink-gray-7 stroke-1.5"
|
||||||
:is="icons.Folder"
|
:is="icons.Folder"
|
||||||
/>
|
/>
|
||||||
<span v-if="selectedIcon">
|
<span v-if="selectedIcon" class="text-ink-gray-7">
|
||||||
{{ selectedIcon }}
|
{{ selectedIcon }}
|
||||||
</span>
|
</span>
|
||||||
<span v-else class="text-ink-gray-5">
|
<span v-else class="text-ink-gray-5">
|
||||||
|
|||||||
@@ -28,10 +28,12 @@
|
|||||||
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
|
class="mt-1 rounded-lg bg-surface-white py-1 text-base border-2"
|
||||||
>
|
>
|
||||||
<ComboboxOptions
|
<ComboboxOptions
|
||||||
class="my-1 min-h-[6rem] max-h-[12rem] overflow-y-auto px-1.5"
|
class="my-1 max-h-[12rem] overflow-y-auto px-1.5"
|
||||||
|
:class="options.length ? 'min-h-[6rem]' : 'min-h-[3.8rem]'"
|
||||||
static
|
static
|
||||||
>
|
>
|
||||||
<ComboboxOption
|
<ComboboxOption
|
||||||
|
v-if="options.length"
|
||||||
v-for="option in options"
|
v-for="option in options"
|
||||||
:key="option.value"
|
:key="option.value"
|
||||||
:value="option"
|
:value="option"
|
||||||
@@ -53,7 +55,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
</ComboboxOption>
|
</ComboboxOption>
|
||||||
<div class="h-10"></div>
|
<div v-else class="text-ink-gray-7 px-4">
|
||||||
|
{{ __('No results found') }}
|
||||||
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="attrs.onCreate"
|
v-if="attrs.onCreate"
|
||||||
class="absolute bottom-2 left-1 w-[95%] pt-2 bg-white border-t"
|
class="absolute bottom-2 left-1 w-[95%] pt-2 bg-white border-t"
|
||||||
|
|||||||
@@ -136,11 +136,11 @@
|
|||||||
import { Award, BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
|
import { Award, BookOpen, GraduationCap, Star, Users } from 'lucide-vue-next'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { Tooltip } from 'frappe-ui'
|
import { Tooltip } from 'frappe-ui'
|
||||||
import { theme } from '@/utils/theme'
|
|
||||||
import { formatAmount } from '@/utils'
|
import { formatAmount } from '@/utils'
|
||||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||||
import UserAvatar from '@/components/UserAvatar.vue'
|
import UserAvatar from '@/components/UserAvatar.vue'
|
||||||
import ProgressBar from '@/components/ProgressBar.vue'
|
import ProgressBar from '@/components/ProgressBar.vue'
|
||||||
|
import colors from '@/utils/frappe-ui-colors.json'
|
||||||
|
|
||||||
const { user } = sessionStore()
|
const { user } = sessionStore()
|
||||||
|
|
||||||
@@ -152,8 +152,10 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const getGradientColor = () => {
|
const getGradientColor = () => {
|
||||||
|
let theme =
|
||||||
|
localStorage.getItem('theme') == 'light' ? 'lightMode' : 'darkMode'
|
||||||
let color = props.course.card_gradient?.toLowerCase() || 'blue'
|
let color = props.course.card_gradient?.toLowerCase() || 'blue'
|
||||||
let colorMap = theme.backgroundColor[color]
|
let colorMap = colors[theme][color]
|
||||||
return `linear-gradient(to top right, black, ${colorMap[400]})`
|
return `linear-gradient(to top right, black, ${colorMap[400]})`
|
||||||
/* return `bg-gradient-to-br from-${color}-100 via-${color}-200 to-${color}-400` */
|
/* return `bg-gradient-to-br from-${color}-100 via-${color}-200 to-${color}-400` */
|
||||||
/* return `linear-gradient(to bottom right, ${colorMap[100]}, ${colorMap[400]})` */
|
/* return `linear-gradient(to bottom right, ${colorMap[100]}, ${colorMap[400]})` */
|
||||||
|
|||||||
@@ -9,5 +9,5 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import AppSidebar from './AppSidebar.vue'
|
import AppSidebar from '@/components/Sidebar/AppSidebar.vue'
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -114,11 +114,11 @@
|
|||||||
categoryColumn: 'category',
|
categoryColumn: 'category',
|
||||||
valueColumn: 'count',
|
valueColumn: 'count',
|
||||||
colors: [
|
colors: [
|
||||||
theme.colors.red['400'],
|
getColor('red', 400),
|
||||||
theme.colors.amber['400'],
|
getColor('amber', 400),
|
||||||
theme.colors.pink['400'],
|
getColor('pink', 400),
|
||||||
theme.colors.blue['400'],
|
getColor('blue', 400),
|
||||||
theme.colors.green['400'],
|
getColor('green', 400),
|
||||||
],
|
],
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@@ -146,7 +146,7 @@ import {
|
|||||||
NumberChart,
|
NumberChart,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { theme } from '@/utils/theme'
|
import { getColor } from '@/utils'
|
||||||
|
|
||||||
const show = defineModel<boolean>({ default: false })
|
const show = defineModel<boolean>({ default: false })
|
||||||
const searchFilter = ref<string | null>(null)
|
const searchFilter = ref<string | null>(null)
|
||||||
|
|||||||
@@ -222,8 +222,8 @@ watch(
|
|||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => profile.language,
|
() => profile.language,
|
||||||
(newVal, oldVal) => {
|
() => {
|
||||||
if (newVal !== oldVal) {
|
if (profile.language !== props.profile.data.language) {
|
||||||
hasLanguageChanged.value = true
|
hasLanguageChanged.value = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,12 +66,18 @@
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
<script setup>
|
<script setup>
|
||||||
import { Dialog, createResource, Select, FormControl, toast } from 'frappe-ui'
|
import {
|
||||||
|
dayjs,
|
||||||
|
Dialog,
|
||||||
|
createResource,
|
||||||
|
Select,
|
||||||
|
FormControl,
|
||||||
|
toast,
|
||||||
|
} from 'frappe-ui'
|
||||||
import { reactive, watch, inject } from 'vue'
|
import { reactive, watch, inject } from 'vue'
|
||||||
import { formatTime } from '@/utils/'
|
import { formatTime } from '@/utils/'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const dayjs = inject('$dayjs')
|
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
const evaluations = defineModel('reloadEvals')
|
const evaluations = defineModel('reloadEvals')
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<Dialog
|
<Dialog
|
||||||
v-model="show"
|
v-model="show"
|
||||||
class="text-base"
|
|
||||||
:options="{
|
:options="{
|
||||||
title: __('Add web page to sidebar'),
|
title: __('Add web page to sidebar'),
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
@@ -17,15 +16,17 @@
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<template #body-content>
|
<template #body-content>
|
||||||
<Link
|
<div class="text-base">
|
||||||
v-model="page.webpage"
|
<Link
|
||||||
doctype="Web Page"
|
v-model="page.webpage"
|
||||||
:label="__('Web Page')"
|
doctype="Web Page"
|
||||||
:filters="{
|
:label="__('Web Page')"
|
||||||
published: 1,
|
:filters="{
|
||||||
}"
|
published: 1,
|
||||||
/>
|
}"
|
||||||
<IconPicker v-model="page.icon" :label="__('Icon')" class="mt-4" />
|
/>
|
||||||
|
<IconPicker v-model="page.icon" :label="__('Icon')" class="mt-4" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -117,32 +117,27 @@ const props = defineProps({
|
|||||||
watch(
|
watch(
|
||||||
() => props.accountID,
|
() => props.accountID,
|
||||||
(val) => {
|
(val) => {
|
||||||
if (val != 'new') {
|
if (val === 'new') {
|
||||||
zoomAccounts.value?.data.forEach((acc) => {
|
account.name = ''
|
||||||
if (acc.name === val) {
|
account.enabled = false
|
||||||
account.name = acc.name
|
account.member = user?.data?.name || ''
|
||||||
account.enabled = acc.enabled || false
|
account.account_id = ''
|
||||||
account.member = acc.member
|
account.client_id = ''
|
||||||
account.account_id = acc.account_id
|
account.client_secret = ''
|
||||||
account.client_id = acc.client_id
|
} else if (val && val !== 'new') {
|
||||||
account.client_secret = acc.client_secret
|
const acc = zoomAccounts.value?.data.find((acc) => acc.name === val)
|
||||||
}
|
if (acc) {
|
||||||
})
|
account.name = acc.name
|
||||||
|
account.enabled = acc.enabled || false
|
||||||
|
account.member = acc.member
|
||||||
|
account.account_id = acc.account_id
|
||||||
|
account.client_id = acc.client_id
|
||||||
|
account.client_secret = acc.client_secret
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(show, (val) => {
|
|
||||||
if (!val) {
|
|
||||||
account.name = ''
|
|
||||||
account.enabled = false
|
|
||||||
account.member = user?.data?.name || ''
|
|
||||||
account.account_id = ''
|
|
||||||
account.client_id = ''
|
|
||||||
account.client_secret = ''
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const saveAccount = (close: () => void) => {
|
const saveAccount = (close: () => void) => {
|
||||||
if (props.accountID == 'new') {
|
if (props.accountID == 'new') {
|
||||||
createAccount(close)
|
createAccount(close)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
<span
|
<span
|
||||||
class="size-3 rounded-full"
|
class="size-3 rounded-full"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundColor: theme.backgroundColor[color.toLowerCase()][400],
|
backgroundColor: getColor(color.toLowerCase(), 400),
|
||||||
}"
|
}"
|
||||||
></span>
|
></span>
|
||||||
<span>
|
<span>
|
||||||
@@ -55,9 +55,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, inject, ref, watch } from 'vue'
|
import { computed, inject, ref, watch } from 'vue'
|
||||||
import { NotepadText, Trash2 } from 'lucide-vue-next'
|
import { NotepadText, Trash2 } from 'lucide-vue-next'
|
||||||
import { theme } from '@/utils/theme'
|
|
||||||
import type { Note, Notes } from '@/components/Notes/types'
|
import type { Note, Notes } from '@/components/Notes/types'
|
||||||
import { blockQuotesClick, highlightText } from '@/utils'
|
import { blockQuotesClick, getColor, highlightText } from '@/utils'
|
||||||
|
|
||||||
const user = inject<any>('$user')
|
const user = inject<any>('$user')
|
||||||
const show = defineModel()
|
const show = defineModel()
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
:placeholder="__('Make notes for quick revision. Press / for menu.')"
|
:placeholder="__('Make notes for quick revision. Press / for menu.')"
|
||||||
@change="(val: string) => updateNoteText(val)"
|
@change="(val: string) => updateNoteText(val)"
|
||||||
:editable="true"
|
:editable="true"
|
||||||
|
:uploadArgs="{
|
||||||
|
private: true,
|
||||||
|
}"
|
||||||
editorClass="prose prose-sm min-h-[200px] max-w-none"
|
editorClass="prose prose-sm min-h-[200px] max-w-none"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -29,7 +29,7 @@
|
|||||||
<div
|
<div
|
||||||
v-if="activeTab && data.doc"
|
v-if="activeTab && data.doc"
|
||||||
:key="activeTab.label"
|
:key="activeTab.label"
|
||||||
class="flex flex-1 flex-col p-8 bg-surface-modal"
|
class="flex flex-1 flex-col p-8 bg-surface-modal overflow-x-auto"
|
||||||
>
|
>
|
||||||
<component
|
<component
|
||||||
v-if="activeTab.template"
|
v-if="activeTab.template"
|
||||||
@@ -71,7 +71,7 @@ import { Dialog, createDocumentResource } from 'frappe-ui'
|
|||||||
import { computed, markRaw, ref, watch } from 'vue'
|
import { computed, markRaw, ref, watch } from 'vue'
|
||||||
import { useSettings } from '@/stores/settings'
|
import { useSettings } from '@/stores/settings'
|
||||||
import SettingDetails from '@/components/Settings/SettingDetails.vue'
|
import SettingDetails from '@/components/Settings/SettingDetails.vue'
|
||||||
import SidebarLink from '@/components/SidebarLink.vue'
|
import SidebarLink from '@/components/Sidebar/SidebarLink.vue'
|
||||||
import Members from '@/components/Settings/Members.vue'
|
import Members from '@/components/Settings/Members.vue'
|
||||||
import Evaluators from '@/components/Settings/Evaluators.vue'
|
import Evaluators from '@/components/Settings/Evaluators.vue'
|
||||||
import Categories from '@/components/Settings/Categories.vue'
|
import Categories from '@/components/Settings/Categories.vue'
|
||||||
@@ -181,6 +181,53 @@ const tabsStructure = computed(() => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Lists',
|
||||||
|
hideLabel: false,
|
||||||
|
items: [
|
||||||
|
{
|
||||||
|
label: 'Members',
|
||||||
|
description:
|
||||||
|
'Add new members or manage roles and permissions of existing members',
|
||||||
|
icon: 'UserRoundPlus',
|
||||||
|
template: markRaw(Members),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Evaluators',
|
||||||
|
description: '',
|
||||||
|
icon: 'UserCheck',
|
||||||
|
description:
|
||||||
|
'Add new evaluators or check the slots existing evaluators',
|
||||||
|
template: markRaw(Evaluators),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Zoom Accounts',
|
||||||
|
description:
|
||||||
|
'Manage zoom accounts to conduct live classes from batches',
|
||||||
|
icon: 'Video',
|
||||||
|
template: markRaw(ZoomSettings),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Badges',
|
||||||
|
description:
|
||||||
|
'Create badges and assign them to students to acknowledge their achievements',
|
||||||
|
icon: 'Award',
|
||||||
|
template: markRaw(Badges),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Categories',
|
||||||
|
description: 'Double click to edit the category',
|
||||||
|
icon: 'Network',
|
||||||
|
template: markRaw(Categories),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Email Templates',
|
||||||
|
description: 'Manage the email templates for your learning system',
|
||||||
|
icon: 'MailPlus',
|
||||||
|
template: markRaw(EmailTemplates),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: 'Payment',
|
label: 'Payment',
|
||||||
hideLabel: false,
|
hideLabel: false,
|
||||||
@@ -242,53 +289,6 @@ const tabsStructure = computed(() => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
|
||||||
label: 'Lists',
|
|
||||||
hideLabel: false,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
label: 'Members',
|
|
||||||
description:
|
|
||||||
'Add new members or manage roles and permissions of existing members',
|
|
||||||
icon: 'UserRoundPlus',
|
|
||||||
template: markRaw(Members),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Evaluators',
|
|
||||||
description: '',
|
|
||||||
icon: 'UserCheck',
|
|
||||||
description:
|
|
||||||
'Add new evaluators or check the slots existing evaluators',
|
|
||||||
template: markRaw(Evaluators),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Zoom Accounts',
|
|
||||||
description:
|
|
||||||
'Manage zoom accounts to conduct live classes from batches',
|
|
||||||
icon: 'Video',
|
|
||||||
template: markRaw(ZoomSettings),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Badges',
|
|
||||||
description:
|
|
||||||
'Create badges and assign them to students to acknowledge their achievements',
|
|
||||||
icon: 'Award',
|
|
||||||
template: markRaw(Badges),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Categories',
|
|
||||||
description: 'Double click to edit the category',
|
|
||||||
icon: 'Network',
|
|
||||||
template: markRaw(Categories),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: 'Email Templates',
|
|
||||||
description: 'Manage the email templates for your learning system',
|
|
||||||
icon: 'MailPlus',
|
|
||||||
template: markRaw(EmailTemplates),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
label: 'Customize',
|
label: 'Customize',
|
||||||
hideLabel: false,
|
hideLabel: false,
|
||||||
|
|||||||
@@ -187,11 +187,12 @@ import { usersStore } from '@/stores/user'
|
|||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { useSidebar } from '@/stores/sidebar'
|
import { useSidebar } from '@/stores/sidebar'
|
||||||
import { useSettings } from '@/stores/settings'
|
import { useSettings } from '@/stores/settings'
|
||||||
import { Button, call, createResource, Tooltip } from 'frappe-ui'
|
import { Button, call, createResource, Tooltip, toast } from 'frappe-ui'
|
||||||
import PageModal from '@/components/Modals/PageModal.vue'
|
import PageModal from '@/components/Modals/PageModal.vue'
|
||||||
import { capture } from '@/telemetry'
|
import { capture } from '@/telemetry'
|
||||||
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
import InviteIcon from '@/components/Icons/InviteIcon.vue'
|
||||||
import {
|
import {
|
||||||
ref,
|
ref,
|
||||||
onMounted,
|
onMounted,
|
||||||
@@ -456,21 +457,13 @@ const openPageModal = (link) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const deletePage = (link) => {
|
const deletePage = (link) => {
|
||||||
createResource({
|
call('lms.lms.api.delete_documents', {
|
||||||
url: 'lms.lms.api.delete_sidebar_item',
|
doctype: 'LMS Sidebar Item',
|
||||||
makeParams(values) {
|
documents: [link.name],
|
||||||
return {
|
}).then(() => {
|
||||||
webpage: link.web_page,
|
sidebarSettings.reload()
|
||||||
}
|
toast.success(__('Page deleted successfully'))
|
||||||
},
|
})
|
||||||
}).submit(
|
|
||||||
{},
|
|
||||||
{
|
|
||||||
onSuccess() {
|
|
||||||
sidebarSettings.reload()
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const toggleSidebar = () => {
|
const toggleSidebar = () => {
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<Popover placement="right-start" class="flex w-full">
|
<Popover placement="right-start" trigger="hover" class="flex w-full">
|
||||||
<template #target="{ togglePopover }">
|
<template #target="{ togglePopover }">
|
||||||
<button
|
<button
|
||||||
:class="[
|
:class="[
|
||||||
'group w-full flex h-7 items-center justify-between rounded px-2 text-base text-ink-gray-7 hover:bg-surface-gray-2',
|
'group w-full flex h-7 items-center justify-between rounded px-2 text-base text-ink-gray-7 hover:bg-surface-gray-2',
|
||||||
]"
|
]"
|
||||||
@click.prevent="togglePopover()"
|
|
||||||
>
|
>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<LayoutGrid class="size-4 stroke-1.5" />
|
<LayoutGrid class="size-4 stroke-1.5" />
|
||||||
26
frontend/src/components/Sidebar/Configuration.vue
Normal file
26
frontend/src/components/Sidebar/Configuration.vue
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<template>
|
||||||
|
<div class="grid grid-cols-3 justify-between bg-surface-white">
|
||||||
|
<div key="name" class="py-1 px-2 hover:bg-surface-gray-2 rounded">
|
||||||
|
<router-link
|
||||||
|
:to="{
|
||||||
|
name: 'DataImportList',
|
||||||
|
query: {
|
||||||
|
step: 'list',
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div class="flex flex-col items-center space-y-1">
|
||||||
|
<ArrowDownToLine
|
||||||
|
class="size-9 text-ink-gray-7 p-2 bg-surface-gray-2 rounded-md"
|
||||||
|
/>
|
||||||
|
<div class="text-sm text-ink-gray-7">
|
||||||
|
{{ __('Import') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ArrowDownToLine } from 'lucide-vue-next'
|
||||||
|
</script>
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-2">
|
<div class="p-2">
|
||||||
<Dropdown :options="userDropdownOptions">
|
<Dropdown :options="userDropdownOptions">
|
||||||
<template v-slot="{ open }">
|
<template v-slot="{ open, close }">
|
||||||
<button
|
<button
|
||||||
class="flex h-12 py-2 items-center rounded-md duration-300 ease-in-out"
|
class="flex h-12 py-2 items-center rounded-md duration-300 ease-in-out"
|
||||||
:class="
|
:class="
|
||||||
@@ -64,18 +64,19 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { Dropdown } from 'frappe-ui'
|
import { Dropdown } from 'frappe-ui'
|
||||||
import Apps from '@/components/Apps.vue'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { convertToTitleCase } from '@/utils'
|
import { convertToTitleCase } from '@/utils'
|
||||||
import { usersStore } from '@/stores/user'
|
import { usersStore } from '@/stores/user'
|
||||||
import { useSettings } from '@/stores/settings'
|
import { useSettings } from '@/stores/settings'
|
||||||
import { markRaw, watch, ref, onMounted, computed } from 'vue'
|
import { markRaw, watch, ref, onMounted, computed } from 'vue'
|
||||||
import { createDialog } from '@/utils/dialogs'
|
import { createDialog } from '@/utils/dialogs'
|
||||||
import SettingsModal from '@/components/Settings/Settings.vue'
|
import Apps from '@/components/Sidebar/Apps.vue'
|
||||||
|
import Configuration from '@/components/Sidebar/Configuration.vue'
|
||||||
import FrappeCloudIcon from '@/components/Icons/FrappeCloudIcon.vue'
|
import FrappeCloudIcon from '@/components/Icons/FrappeCloudIcon.vue'
|
||||||
|
import LMSLogo from '@/components/Icons/LMSLogo.vue'
|
||||||
|
import SettingsModal from '@/components/Settings/Settings.vue'
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
LogIn,
|
LogIn,
|
||||||
@@ -84,6 +85,7 @@ import {
|
|||||||
User,
|
User,
|
||||||
Settings,
|
Settings,
|
||||||
Sun,
|
Sun,
|
||||||
|
Wrench,
|
||||||
Zap,
|
Zap,
|
||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
|
|
||||||
@@ -168,6 +170,18 @@ const userDropdownOptions = computed(() => {
|
|||||||
return userResource.data?.is_moderator
|
return userResource.data?.is_moderator
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: 'Configuration',
|
||||||
|
icon: Wrench,
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
component: markRaw(Configuration),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
condition: () => {
|
||||||
|
return userResource.data?.is_moderator
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: FrappeCloudIcon,
|
icon: FrappeCloudIcon,
|
||||||
label: 'Login to Frappe Cloud',
|
label: 'Login to Frappe Cloud',
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
import { createResource } from 'frappe-ui'
|
import { createResource } from 'frappe-ui'
|
||||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||||
import ApexChart from 'vue3-apexcharts'
|
import ApexChart from 'vue3-apexcharts'
|
||||||
import { theme } from '@/utils/theme'
|
import { getColor } from '@/utils'
|
||||||
|
|
||||||
const user = inject('$user')
|
const user = inject('$user')
|
||||||
const labels = ref([])
|
const labels = ref([])
|
||||||
@@ -81,11 +81,11 @@ const chartOptions = computed(() => {
|
|||||||
enableShades: true,
|
enableShades: true,
|
||||||
colorScale: {
|
colorScale: {
|
||||||
ranges: [
|
ranges: [
|
||||||
{ from: 0, to: 0, color: theme.colors.gray[400] },
|
{ from: 0, to: 0, color: getColor('green', 400) },
|
||||||
{ from: 1, to: 5, color: theme.colors.green[200] },
|
{ from: 1, to: 5, color: getColor('green', 200) },
|
||||||
{ from: 6, to: 15, color: theme.colors.green[500] },
|
{ from: 6, to: 15, color: getColor('green', 500) },
|
||||||
{ from: 16, to: 30, color: theme.colors.green[700] },
|
{ from: 16, to: 30, color: getColor('green', 700) },
|
||||||
{ from: 31, to: 100, color: theme.colors.green[800] },
|
{ from: 31, to: 100, color: getColor('green', 800) },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
@import './assets/Inter/inter.css';
|
@import './assets/Inter/inter.css';
|
||||||
@import 'frappe-ui/src/style.css';
|
@import 'frappe-ui/style.css';
|
||||||
@import './styles/codemirror.css';
|
@import './styles/codemirror.css';
|
||||||
@@ -3,7 +3,49 @@
|
|||||||
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
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" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
<router-link
|
<Dropdown
|
||||||
|
v-if="canCreateBatch()"
|
||||||
|
:options="[
|
||||||
|
{
|
||||||
|
label: __('New Batch'),
|
||||||
|
icon: 'users',
|
||||||
|
onClick() {
|
||||||
|
router.push({
|
||||||
|
name: 'BatchForm',
|
||||||
|
params: { batchName: 'new' },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Import Batch'),
|
||||||
|
icon: 'upload',
|
||||||
|
onClick() {
|
||||||
|
router.push({
|
||||||
|
name: 'NewDataImport',
|
||||||
|
params: { doctype: 'LMS Batch' },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<template v-slot="{ open }">
|
||||||
|
<Button variant="solid">
|
||||||
|
<template #prefix>
|
||||||
|
<Plus class="h-4 w-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Create') }}
|
||||||
|
<template #suffix>
|
||||||
|
<ChevronDown
|
||||||
|
:class="[
|
||||||
|
'w-4 h-4 stroke-1.5 ml-1 transform transition-transform',
|
||||||
|
open ? 'rotate-180' : '',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
|
<!-- <router-link
|
||||||
v-if="canCreateBatch()"
|
v-if="canCreateBatch()"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'BatchForm',
|
name: 'BatchForm',
|
||||||
@@ -16,7 +58,7 @@
|
|||||||
</template>
|
</template>
|
||||||
{{ __('Create') }}
|
{{ __('Create') }}
|
||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link> -->
|
||||||
</header>
|
</header>
|
||||||
<div class="p-5 pb-10">
|
<div class="p-5 pb-10">
|
||||||
<div
|
<div
|
||||||
@@ -90,13 +132,15 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
call,
|
call,
|
||||||
createListResource,
|
createListResource,
|
||||||
|
Dropdown,
|
||||||
FormControl,
|
FormControl,
|
||||||
Select,
|
Select,
|
||||||
TabButtons,
|
TabButtons,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||||
import { Plus } from 'lucide-vue-next'
|
import { useRouter } from 'vue-router'
|
||||||
|
import { ChevronDown, Plus } from 'lucide-vue-next'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import BatchCard from '@/components/BatchCard.vue'
|
import BatchCard from '@/components/BatchCard.vue'
|
||||||
import EmptyState from '@/components/EmptyState.vue'
|
import EmptyState from '@/components/EmptyState.vue'
|
||||||
@@ -115,6 +159,7 @@ const is_student = computed(() => user.data?.is_student)
|
|||||||
const currentTab = ref(is_student.value ? 'All' : 'Upcoming')
|
const currentTab = ref(is_student.value ? 'All' : 'Upcoming')
|
||||||
const orderBy = ref('start_date')
|
const orderBy = ref('start_date')
|
||||||
const readOnlyMode = window.read_only_mode
|
const readOnlyMode = window.read_only_mode
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
setFiltersFromQuery()
|
setFiltersFromQuery()
|
||||||
|
|||||||
@@ -3,20 +3,51 @@
|
|||||||
class="sticky flex items-center justify-between top-0 z-10 border-b bg-surface-white px-3 py-2.5 sm:px-5"
|
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" />
|
<Breadcrumbs :items="breadcrumbs" />
|
||||||
<router-link
|
|
||||||
|
<Dropdown
|
||||||
|
placement="start"
|
||||||
|
side="bottom"
|
||||||
v-if="canCreateCourse()"
|
v-if="canCreateCourse()"
|
||||||
:to="{
|
:options="[
|
||||||
name: 'CourseForm',
|
{
|
||||||
params: { courseName: 'new' },
|
label: __('New Course'),
|
||||||
}"
|
icon: 'book-open',
|
||||||
|
onClick() {
|
||||||
|
router.push({
|
||||||
|
name: 'CourseForm',
|
||||||
|
params: { courseName: 'new' },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: __('Import Course'),
|
||||||
|
icon: 'upload',
|
||||||
|
onClick() {
|
||||||
|
router.push({
|
||||||
|
name: 'NewDataImport',
|
||||||
|
params: { doctype: 'LMS Course' },
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]"
|
||||||
>
|
>
|
||||||
<Button variant="solid">
|
<template v-slot="{ open }">
|
||||||
<template #prefix>
|
<Button variant="solid">
|
||||||
<Plus class="h-4 w-4 stroke-1.5" />
|
<template #prefix>
|
||||||
</template>
|
<Plus class="h-4 w-4 stroke-1.5" />
|
||||||
{{ __('Create') }}
|
</template>
|
||||||
</Button>
|
{{ __('Create') }}
|
||||||
</router-link>
|
<template #suffix>
|
||||||
|
<ChevronDown
|
||||||
|
:class="[
|
||||||
|
'w-4 h-4 stroke-1.5 ml-1 transform transition-transform',
|
||||||
|
open ? 'rotate-180' : '',
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
</Dropdown>
|
||||||
</header>
|
</header>
|
||||||
<div class="p-5 pb-10">
|
<div class="p-5 pb-10">
|
||||||
<div
|
<div
|
||||||
@@ -85,13 +116,14 @@ import {
|
|||||||
Button,
|
Button,
|
||||||
call,
|
call,
|
||||||
createListResource,
|
createListResource,
|
||||||
|
Dropdown,
|
||||||
FormControl,
|
FormControl,
|
||||||
Select,
|
Select,
|
||||||
TabButtons,
|
TabButtons,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||||
import { Plus } from 'lucide-vue-next'
|
import { ChevronDown, Plus } from 'lucide-vue-next'
|
||||||
import { sessionStore } from '@/stores/session'
|
import { sessionStore } from '@/stores/session'
|
||||||
import { canCreateCourse } from '@/utils'
|
import { canCreateCourse } from '@/utils'
|
||||||
import CourseCard from '@/components/CourseCard.vue'
|
import CourseCard from '@/components/CourseCard.vue'
|
||||||
|
|||||||
50
frontend/src/pages/DataImport.vue
Normal file
50
frontend/src/pages/DataImport.vue
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
<template>
|
||||||
|
<DataImport
|
||||||
|
:doctype="route.params.doctype"
|
||||||
|
:importName="route.params.importName"
|
||||||
|
:doctypeMap="doctypeMap"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { usePageMeta } from 'frappe-ui'
|
||||||
|
import { DataImport } from 'frappe-ui/frappe'
|
||||||
|
import { sessionStore } from '../stores/session'
|
||||||
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
|
import { inject, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const { brand } = sessionStore()
|
||||||
|
const route = useRoute()
|
||||||
|
const router = useRouter()
|
||||||
|
const user = inject<any>('$user')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (!user.data?.is_moderator) {
|
||||||
|
router.push({
|
||||||
|
name: 'Courses',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const doctypeMap = {
|
||||||
|
'LMS Course': {
|
||||||
|
title: 'Courses',
|
||||||
|
listRoute: '/courses',
|
||||||
|
pageRoute: `/courses/docname`,
|
||||||
|
},
|
||||||
|
'LMS Batch': {
|
||||||
|
title: 'Batches',
|
||||||
|
listRoute: '/batches',
|
||||||
|
},
|
||||||
|
'LMS Category': {
|
||||||
|
title: 'Categories',
|
||||||
|
listRoute: '/lms',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
usePageMeta(() => {
|
||||||
|
return {
|
||||||
|
title: __('Data Import'),
|
||||||
|
icon: brand.favicon,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<div v-if="createdBatches.data?.length" class="mt-10">
|
<div v-if="createdBatches.data?.length" class="mt-10">
|
||||||
<div class="flex items-center justify-between mb-3">
|
<div class="flex items-center justify-between mb-3">
|
||||||
<span class="font-semibold text-lg">
|
<span class="font-semibold text-lg text-ink-gray-9">
|
||||||
{{ __('Upcoming Batches') }}
|
{{ __('Upcoming Batches') }}
|
||||||
</span>
|
</span>
|
||||||
<router-link
|
<router-link
|
||||||
@@ -88,7 +88,7 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-2 gap-5 mt-10">
|
<div class="grid grid-cols-2 gap-5 mt-10">
|
||||||
<div v-if="evals?.data?.length">
|
<div v-if="evals?.data?.length">
|
||||||
<div class="font-semibold text-lg mb-3">
|
<div class="font-semibold text-lg text-ink-gray-9 mb-3">
|
||||||
{{ __('Upcoming Evaluations') }}
|
{{ __('Upcoming Evaluations') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||||
@@ -124,7 +124,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="liveClasses?.data?.length">
|
<div v-if="liveClasses?.data?.length">
|
||||||
<div class="font-semibold text-lg mb-3">
|
<div class="font-semibold text-lg text-ink-gray-9 mb-3">
|
||||||
{{ __('Upcoming Live Classes') }}
|
{{ __('Upcoming Live Classes') }}
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-5">
|
||||||
|
|||||||
@@ -17,86 +17,91 @@
|
|||||||
</header>
|
</header>
|
||||||
<div class="max-w-4xl mx-auto pt-5 p-4">
|
<div class="max-w-4xl mx-auto pt-5 p-4">
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h1 class="text-lg font-semibold text-ink-gray-9 mb-2">
|
<h1 class="text-xl font-semibold text-ink-gray-7 mb-4 md:mb-0">
|
||||||
{{ applications.data?.length || 0 }}
|
{{ applicationCount }}
|
||||||
{{
|
{{ applicationCount === 1 ? __('Application') : __('Applications') }}
|
||||||
applications.data?.length === 1
|
|
||||||
? __('Application')
|
|
||||||
: __('Applications')
|
|
||||||
}}
|
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ListView
|
<div v-if="applications.data?.length">
|
||||||
v-if="applications.data?.length"
|
<ListView
|
||||||
:columns="applicationColumns"
|
:columns="applicationColumns"
|
||||||
:rows="applicantRows"
|
:rows="applicantRows"
|
||||||
row-key="name"
|
row-key="name"
|
||||||
:options="{
|
:options="{
|
||||||
showTooltip: false,
|
showTooltip: false,
|
||||||
selectable: false,
|
selectable: false,
|
||||||
}"
|
}"
|
||||||
>
|
|
||||||
<ListHeader
|
|
||||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
|
||||||
>
|
>
|
||||||
<ListHeaderItem
|
<ListHeader
|
||||||
:item="item"
|
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||||
v-for="item in applicationColumns"
|
|
||||||
:key="item.key"
|
|
||||||
>
|
>
|
||||||
<template #prefix="{ item }">
|
<ListHeaderItem
|
||||||
<FeatherIcon
|
:item="item"
|
||||||
v-if="item.icon"
|
v-for="item in applicationColumns"
|
||||||
:name="item.icon?.toString()"
|
:key="item.key"
|
||||||
class="h-4 w-4"
|
>
|
||||||
/>
|
<template #prefix="{ item }">
|
||||||
</template>
|
<FeatherIcon
|
||||||
</ListHeaderItem>
|
v-if="item.icon"
|
||||||
</ListHeader>
|
:name="item.icon?.toString()"
|
||||||
<ListRows>
|
class="h-4 w-4"
|
||||||
<ListRow
|
|
||||||
:row="row"
|
|
||||||
v-slot="{ column, item }"
|
|
||||||
v-for="row in applicantRows"
|
|
||||||
class="cursor-pointer"
|
|
||||||
>
|
|
||||||
<ListRowItem :item="item">
|
|
||||||
<div
|
|
||||||
v-if="column.key === 'full_name'"
|
|
||||||
class="flex items-center space-x-3"
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
size="sm"
|
|
||||||
:image="row['user_image']"
|
|
||||||
:label="row['full_name']"
|
|
||||||
/>
|
/>
|
||||||
|
</template>
|
||||||
|
</ListHeaderItem>
|
||||||
|
</ListHeader>
|
||||||
|
<ListRows>
|
||||||
|
<ListRow
|
||||||
|
:row="row"
|
||||||
|
v-slot="{ column, item }"
|
||||||
|
v-for="row in applicantRows"
|
||||||
|
class="cursor-pointer"
|
||||||
|
>
|
||||||
|
<ListRowItem :item="item">
|
||||||
|
<div
|
||||||
|
v-if="column.key === 'full_name'"
|
||||||
|
class="flex items-center space-x-3"
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
size="sm"
|
||||||
|
:image="row['user_image']"
|
||||||
|
:label="row['full_name']"
|
||||||
|
/>
|
||||||
|
|
||||||
<span>{{ item }}</span>
|
<span>{{ item }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="column.key === 'actions'"
|
v-else-if="column.key === 'actions'"
|
||||||
class="flex justify-center"
|
class="flex justify-center"
|
||||||
>
|
>
|
||||||
<Dropdown :options="getActionOptions(row)">
|
<Dropdown :options="getActionOptions(row)">
|
||||||
<Button variant="ghost">
|
<Button variant="ghost">
|
||||||
<FeatherIcon name="more-horizontal" class="w-4 h-4" />
|
<FeatherIcon name="more-horizontal" class="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-else-if="column.key === 'applied_on'"
|
v-else-if="column.key === 'applied_on'"
|
||||||
class="text-sm text-ink-gray-6"
|
class="text-sm text-ink-gray-6"
|
||||||
>
|
>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
{{ item }}
|
{{ item }}
|
||||||
</div>
|
</div>
|
||||||
</ListRowItem>
|
</ListRowItem>
|
||||||
</ListRow>
|
</ListRow>
|
||||||
</ListRows>
|
</ListRows>
|
||||||
</ListView>
|
</ListView>
|
||||||
|
<div class="flex justify-center mt-5">
|
||||||
|
<Button v-if="applications.hasNextPage" @click="applications.next()">
|
||||||
|
<template #prefix>
|
||||||
|
<RefreshCw class="size-4 stroke-1.5" />
|
||||||
|
</template>
|
||||||
|
{{ __('Load More') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<EmptyState v-else-if="!applications.loading" type="Job Applications" />
|
<EmptyState v-else-if="!applications.loading" type="Job Applications" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -150,6 +155,7 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
Breadcrumbs,
|
Breadcrumbs,
|
||||||
|
call,
|
||||||
Dialog,
|
Dialog,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
FeatherIcon,
|
FeatherIcon,
|
||||||
@@ -166,8 +172,8 @@ import {
|
|||||||
usePageMeta,
|
usePageMeta,
|
||||||
toast,
|
toast,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
|
import { RefreshCw } from 'lucide-vue-next'
|
||||||
import { inject, ref, computed, reactive } from 'vue'
|
import { computed, inject, onMounted, ref, reactive } from 'vue'
|
||||||
import { sessionStore } from '../stores/session'
|
import { sessionStore } from '../stores/session'
|
||||||
import EmptyState from '@/components/EmptyState.vue'
|
import EmptyState from '@/components/EmptyState.vue'
|
||||||
|
|
||||||
@@ -175,6 +181,7 @@ const dayjs = inject('$dayjs')
|
|||||||
const { brand } = sessionStore()
|
const { brand } = sessionStore()
|
||||||
const showEmailModal = ref(false)
|
const showEmailModal = ref(false)
|
||||||
const selectedApplicant = ref(null)
|
const selectedApplicant = ref(null)
|
||||||
|
const applicationCount = ref(0)
|
||||||
const emailForm = reactive({
|
const emailForm = reactive({
|
||||||
subject: '',
|
subject: '',
|
||||||
message: '',
|
message: '',
|
||||||
@@ -188,6 +195,19 @@ const props = defineProps({
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
getApplicationCount()
|
||||||
|
})
|
||||||
|
|
||||||
|
const getApplicationCount = () => {
|
||||||
|
call('frappe.client.get_count', {
|
||||||
|
doctype: 'LMS Job Application',
|
||||||
|
filters: { job: props.job },
|
||||||
|
}).then((count) => {
|
||||||
|
applicationCount.value = count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const applications = createListResource({
|
const applications = createListResource({
|
||||||
doctype: 'LMS Job Application',
|
doctype: 'LMS Job Application',
|
||||||
fields: [
|
fields: [
|
||||||
@@ -253,7 +273,6 @@ const sendEmail = (close) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const downloadResume = (resumeUrl) => {
|
const downloadResume = (resumeUrl) => {
|
||||||
console.log(resumeUrl)
|
|
||||||
window.open(resumeUrl, '_blank')
|
window.open(resumeUrl, '_blank')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</router-link>
|
</router-link>
|
||||||
<router-link
|
<router-link
|
||||||
v-if="user.data.name == job.data?.owner"
|
v-if="canManageJob"
|
||||||
:to="{
|
:to="{
|
||||||
name: 'JobForm',
|
name: 'JobForm',
|
||||||
params: { jobName: job.data?.name },
|
params: { jobName: job.data?.name },
|
||||||
@@ -240,9 +240,7 @@ const redirectToWebsite = (url) => {
|
|||||||
|
|
||||||
const canManageJob = computed(() => {
|
const canManageJob = computed(() => {
|
||||||
if (!user.data?.name || !job.data) return false
|
if (!user.data?.name || !job.data) return false
|
||||||
return (
|
return user.data.name === job.data.owner || user.data?.is_moderator
|
||||||
user.data.name === job.data.owner || user.data.roles?.includes('Moderator')
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
usePageMeta(() => {
|
usePageMeta(() => {
|
||||||
|
|||||||
@@ -207,6 +207,11 @@ const jobDetail = createResource({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onSuccess(data) {
|
onSuccess(data) {
|
||||||
|
if (data.owner != user.data?.name && !user.data?.is_moderator) {
|
||||||
|
router.push({
|
||||||
|
name: 'Jobs',
|
||||||
|
})
|
||||||
|
}
|
||||||
Object.keys(data).forEach((key) => {
|
Object.keys(data).forEach((key) => {
|
||||||
if (Object.hasOwn(job, key)) job[key] = data[key]
|
if (Object.hasOwn(job, key)) job[key] = data[key]
|
||||||
})
|
})
|
||||||
@@ -242,7 +247,11 @@ const job = reactive({
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!user.data) window.location.href = '/login'
|
if (!user.data) {
|
||||||
|
router.push({
|
||||||
|
name: 'Jobs',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if (props.jobName != 'new') jobDetail.reload()
|
if (props.jobName != 'new') jobDetail.reload()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -32,10 +32,13 @@
|
|||||||
{{ __('{0} Open Jobs').format(jobCount) }}
|
{{ __('{0} Open Jobs').format(jobCount) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div
|
<div class="flex items-center justify-between space-x-4">
|
||||||
class="grid grid-cols-1 gap-2 md:grid-cols-4"
|
<TabButtons
|
||||||
:class="user.data ? 'md:grid-cols-3' : 'md:grid-cols-2'"
|
v-if="tabs.length > 1"
|
||||||
>
|
v-model="activeTab"
|
||||||
|
:buttons="tabs"
|
||||||
|
@change="updateJobs"
|
||||||
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
type="text"
|
type="text"
|
||||||
:placeholder="__('Search')"
|
:placeholder="__('Search')"
|
||||||
@@ -55,13 +58,13 @@
|
|||||||
doctype="Country"
|
doctype="Country"
|
||||||
v-model="country"
|
v-model="country"
|
||||||
:placeholder="__('Country')"
|
:placeholder="__('Country')"
|
||||||
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
|
||||||
/>
|
/>
|
||||||
<FormControl
|
<FormControl
|
||||||
v-model="jobType"
|
v-model="jobType"
|
||||||
type="select"
|
type="select"
|
||||||
:options="jobTypes"
|
:options="jobTypes"
|
||||||
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
|
||||||
:placeholder="__('Type')"
|
:placeholder="__('Type')"
|
||||||
@change="updateJobs"
|
@change="updateJobs"
|
||||||
/>
|
/>
|
||||||
@@ -69,7 +72,7 @@
|
|||||||
v-model="workMode"
|
v-model="workMode"
|
||||||
type="select"
|
type="select"
|
||||||
:options="workModes"
|
:options="workModes"
|
||||||
class="min-w-40 lg:min-w-0 lg:w-32 xl:w-40"
|
class="min-w-32 lg:min-w-0 lg:w-32 xl:w-32"
|
||||||
:placeholder="__('Work Mode')"
|
:placeholder="__('Work Mode')"
|
||||||
@change="updateJobs"
|
@change="updateJobs"
|
||||||
/>
|
/>
|
||||||
@@ -100,6 +103,7 @@ import {
|
|||||||
call,
|
call,
|
||||||
createResource,
|
createResource,
|
||||||
FormControl,
|
FormControl,
|
||||||
|
TabButtons,
|
||||||
usePageMeta,
|
usePageMeta,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { Plus, Search } from 'lucide-vue-next'
|
import { Plus, Search } from 'lucide-vue-next'
|
||||||
@@ -118,9 +122,38 @@ const country = ref(null)
|
|||||||
const filters = ref({})
|
const filters = ref({})
|
||||||
const orFilters = ref({})
|
const orFilters = ref({})
|
||||||
const jobCount = ref(0)
|
const jobCount = ref(0)
|
||||||
|
const closedJobs = ref(0)
|
||||||
|
const activeTab = ref('Open')
|
||||||
const readOnlyMode = window.read_only_mode
|
const readOnlyMode = window.read_only_mode
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
getClosedJobCount()
|
||||||
|
setFiltersFromURL()
|
||||||
|
updateJobs()
|
||||||
|
})
|
||||||
|
|
||||||
|
const isModerator = computed(() => {
|
||||||
|
return user.data?.is_moderator
|
||||||
|
})
|
||||||
|
|
||||||
|
const getClosedJobCount = () => {
|
||||||
|
const filters = {
|
||||||
|
status: 'Closed',
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isModerator.value) {
|
||||||
|
filters.owner = user.data?.name
|
||||||
|
}
|
||||||
|
|
||||||
|
call('frappe.client.get_count', {
|
||||||
|
doctype: 'Job Opportunity',
|
||||||
|
filters: filters,
|
||||||
|
}).then((count) => {
|
||||||
|
closedJobs.value = count
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const setFiltersFromURL = () => {
|
||||||
let queries = new URLSearchParams(location.search)
|
let queries = new URLSearchParams(location.search)
|
||||||
if (queries.has('type')) {
|
if (queries.has('type')) {
|
||||||
jobType.value = queries.get('type')
|
jobType.value = queries.get('type')
|
||||||
@@ -128,7 +161,22 @@ onMounted(() => {
|
|||||||
if (queries.has('work_mode')) {
|
if (queries.has('work_mode')) {
|
||||||
workMode.value = queries.get('work_mode')
|
workMode.value = queries.get('work_mode')
|
||||||
}
|
}
|
||||||
updateJobs()
|
}
|
||||||
|
|
||||||
|
const tabs = computed(() => {
|
||||||
|
const tabsArray = [
|
||||||
|
{
|
||||||
|
label: __('Open'),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
if (closedJobs.value) {
|
||||||
|
tabsArray.push({
|
||||||
|
label: __('Closed'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return tabsArray
|
||||||
})
|
})
|
||||||
|
|
||||||
const jobs = createResource({
|
const jobs = createResource({
|
||||||
@@ -149,7 +197,6 @@ const updateJobs = () => {
|
|||||||
|
|
||||||
const updateFilters = () => {
|
const updateFilters = () => {
|
||||||
filters.value.status = 'Open'
|
filters.value.status = 'Open'
|
||||||
filters.value.disabled = 0
|
|
||||||
|
|
||||||
if (jobType.value) {
|
if (jobType.value) {
|
||||||
filters.value.type = jobType.value
|
filters.value.type = jobType.value
|
||||||
@@ -178,8 +225,22 @@ const updateFilters = () => {
|
|||||||
} else {
|
} else {
|
||||||
delete filters.value.country
|
delete filters.value.country
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (activeTab.value === 'Closed') {
|
||||||
|
filters.value.status = 'Closed'
|
||||||
|
if (!isModerator.value) {
|
||||||
|
filters.value.owner = user.data?.name
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
filters.value.status = 'Open'
|
||||||
|
delete filters.value.owner
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(activeTab, (val) => {
|
||||||
|
updateJobs()
|
||||||
|
})
|
||||||
|
|
||||||
watch(country, (val) => {
|
watch(country, (val) => {
|
||||||
updateJobs()
|
updateJobs()
|
||||||
})
|
})
|
||||||
@@ -190,7 +251,7 @@ watch(jobs, () => {
|
|||||||
|
|
||||||
const jobTypes = computed(() => {
|
const jobTypes = computed(() => {
|
||||||
return [
|
return [
|
||||||
'',
|
{ label: '', value: '' },
|
||||||
{ label: __('Full Time'), value: 'Full Time' },
|
{ label: __('Full Time'), value: 'Full Time' },
|
||||||
{ label: __('Part Time'), value: 'Part Time' },
|
{ label: __('Part Time'), value: 'Part Time' },
|
||||||
{ label: __('Contract'), value: 'Contract' },
|
{ label: __('Contract'), value: 'Contract' },
|
||||||
@@ -200,7 +261,7 @@ const jobTypes = computed(() => {
|
|||||||
|
|
||||||
const workModes = computed(() => {
|
const workModes = computed(() => {
|
||||||
return [
|
return [
|
||||||
'',
|
{ label: '', value: '' },
|
||||||
{ label: 'On site', value: 'On-site' },
|
{ label: 'On site', value: 'On-site' },
|
||||||
{ label: 'Hybrid', value: 'Hybrid' },
|
{ label: 'Hybrid', value: 'Hybrid' },
|
||||||
{ label: 'Remote', value: 'Remote' },
|
{ label: 'Remote', value: 'Remote' },
|
||||||
|
|||||||
@@ -58,15 +58,15 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<ListView
|
<ListView
|
||||||
v-if="programCourses.data.length > 0"
|
v-if="program.program_courses?.length > 0"
|
||||||
:columns="courseColumns"
|
:columns="courseColumns"
|
||||||
:rows="programCourses.data"
|
:rows="program.program_courses"
|
||||||
:options="{
|
:options="{
|
||||||
selectable: true,
|
selectable: true,
|
||||||
resizeColumn: true,
|
resizeColumn: true,
|
||||||
showTooltip: false,
|
showTooltip: false,
|
||||||
}"
|
}"
|
||||||
rowKey="name"
|
:rowKey="programName === 'new' ? 'course' : 'name'"
|
||||||
>
|
>
|
||||||
<ListHeader
|
<ListHeader
|
||||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||||
@@ -75,8 +75,8 @@
|
|||||||
</ListHeader>
|
</ListHeader>
|
||||||
<ListRows>
|
<ListRows>
|
||||||
<Draggable
|
<Draggable
|
||||||
:list="programCourses.data"
|
:list="program.program_courses"
|
||||||
item-key="name"
|
:item-key="programName === 'new' ? 'course' : 'name'"
|
||||||
group="items"
|
group="items"
|
||||||
@end="updateOrder"
|
@end="updateOrder"
|
||||||
class="cursor-move"
|
class="cursor-move"
|
||||||
@@ -133,14 +133,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ListView
|
<ListView
|
||||||
v-if="programMembers.data.length > 0"
|
v-if="program.program_members?.length > 0"
|
||||||
:columns="memberColumns"
|
:columns="memberColumns"
|
||||||
:rows="programMembers.data"
|
:rows="program.program_members"
|
||||||
:options="{
|
:options="{
|
||||||
selectable: true,
|
selectable: true,
|
||||||
resizeColumn: true,
|
resizeColumn: true,
|
||||||
}"
|
}"
|
||||||
rowKey="name"
|
:rowKey="programName === 'new' ? 'member' : 'name'"
|
||||||
>
|
>
|
||||||
<ListHeader
|
<ListHeader
|
||||||
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
class="mb-2 grid items-center space-x-4 rounded bg-surface-gray-2 p-2"
|
||||||
@@ -148,7 +148,7 @@
|
|||||||
<ListHeaderItem :item="item" v-for="item in memberColumns" />
|
<ListHeaderItem :item="item" v-for="item in memberColumns" />
|
||||||
</ListHeader>
|
</ListHeader>
|
||||||
<ListRows>
|
<ListRows>
|
||||||
<ListRow :row="row" v-for="row in programMembers.data" />
|
<ListRow :row="row" v-for="row in program.program_members" />
|
||||||
</ListRows>
|
</ListRows>
|
||||||
<ListSelectBanner>
|
<ListSelectBanner>
|
||||||
<template #actions="{ unselectAll, selections }">
|
<template #actions="{ unselectAll, selections }">
|
||||||
@@ -217,13 +217,12 @@
|
|||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template #actions="{ close }">
|
<template #actions="{ close }">
|
||||||
<div class="flex justify-end space-x-2 group">
|
<div class="flex justify-end space-x-2">
|
||||||
<Button
|
<Button
|
||||||
v-if="programName != 'new'"
|
v-if="programName != 'new'"
|
||||||
@click="deleteProgram(close)"
|
@click="deleteProgram(close)"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
theme="red"
|
theme="red"
|
||||||
class="invisible group-hover:visible"
|
|
||||||
>
|
>
|
||||||
<template #prefix>
|
<template #prefix>
|
||||||
<Trash2 class="size-4 stroke-1.5" />
|
<Trash2 class="size-4 stroke-1.5" />
|
||||||
@@ -252,7 +251,7 @@ import {
|
|||||||
ListRow,
|
ListRow,
|
||||||
toast,
|
toast,
|
||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch, getCurrentInstance } from 'vue'
|
||||||
import { Plus, Trash2, TrendingUp } from 'lucide-vue-next'
|
import { Plus, Trash2, TrendingUp } from 'lucide-vue-next'
|
||||||
import { Programs, Program } from '@/types/programs'
|
import { Programs, Program } from '@/types/programs'
|
||||||
import { escapeHTML, openSettings } from '@/utils'
|
import { escapeHTML, openSettings } from '@/utils'
|
||||||
@@ -269,6 +268,9 @@ const member = ref<string>('')
|
|||||||
const showProgressDialog = ref(false)
|
const showProgressDialog = ref(false)
|
||||||
const dirty = ref(false)
|
const dirty = ref(false)
|
||||||
|
|
||||||
|
const app = getCurrentInstance()
|
||||||
|
const { $dialog } = app.appContext.config.globalProperties
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
programName: string | null
|
programName: string | null
|
||||||
@@ -427,25 +429,22 @@ const addCourse = (close: () => void) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
programCourses.insert.submit(
|
const existingCourse = program.value.program_courses.find(
|
||||||
{
|
(c: any) => c.course === course.value
|
||||||
parent: props.programName,
|
|
||||||
parenttype: 'LMS Program',
|
|
||||||
parentfield: 'program_courses',
|
|
||||||
course: course.value,
|
|
||||||
idx: programCourses.data.length + 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess() {
|
|
||||||
updateCounts('course', 'add')
|
|
||||||
close()
|
|
||||||
toast.success(__('Course added to program successfully'))
|
|
||||||
},
|
|
||||||
onError(err: any) {
|
|
||||||
toast.warning(__(err.messages?.[0] || err))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
if (!existingCourse) {
|
||||||
|
program.value.program_courses.push({
|
||||||
|
course: course.value,
|
||||||
|
idx: program.value.program_courses.length + 1,
|
||||||
|
})
|
||||||
|
if (props.programName !== 'new') {
|
||||||
|
dirty.value = true
|
||||||
|
}
|
||||||
|
close()
|
||||||
|
toast.success(__('Course added to program successfully'))
|
||||||
|
} else {
|
||||||
|
toast.warning(__('Course already added to program'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const addMember = (close: () => void) => {
|
const addMember = (close: () => void) => {
|
||||||
@@ -454,24 +453,21 @@ const addMember = (close: () => void) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
programMembers.insert.submit(
|
const existingMember = program.value.program_members.find(
|
||||||
{
|
(m) => m.member === member.value
|
||||||
parent: props.programName,
|
|
||||||
parenttype: 'LMS Program',
|
|
||||||
parentfield: 'program_members',
|
|
||||||
member: member.value,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
onSuccess() {
|
|
||||||
updateCounts('member', 'add')
|
|
||||||
close()
|
|
||||||
toast.success(__('Member added to program successfully'))
|
|
||||||
},
|
|
||||||
onError(err: any) {
|
|
||||||
toast.warning(__(err.messages?.[0] || err))
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
if (!existingMember) {
|
||||||
|
program.value.program_members.push({
|
||||||
|
member: member.value,
|
||||||
|
})
|
||||||
|
if (props.programName !== 'new') {
|
||||||
|
dirty.value = true
|
||||||
|
}
|
||||||
|
close()
|
||||||
|
toast.success(__('Member added to program successfully'))
|
||||||
|
} else {
|
||||||
|
toast.warning(__('Member already added to program'))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateCounts = async (
|
const updateCounts = async (
|
||||||
@@ -509,57 +505,83 @@ const updateCounts = async (
|
|||||||
const updateOrder = async (e: DragEvent) => {
|
const updateOrder = async (e: DragEvent) => {
|
||||||
let sourceIdx = e.from.dataset.idx
|
let sourceIdx = e.from.dataset.idx
|
||||||
let targetIdx = e.to.dataset.idx
|
let targetIdx = e.to.dataset.idx
|
||||||
let courses = programCourses.data
|
|
||||||
courses.splice(targetIdx, 0, courses.splice(sourceIdx, 1)[0])
|
|
||||||
|
|
||||||
for (const [index, course] of courses.entries()) {
|
if (props.programName === 'new') {
|
||||||
programCourses.setValue.submit(
|
let courses = program.value.program_courses
|
||||||
{
|
courses.splice(targetIdx, 0, courses.splice(sourceIdx, 1)[0])
|
||||||
name: course.name,
|
courses.forEach((course, index) => {
|
||||||
idx: index + 1,
|
course.idx = index + 1
|
||||||
},
|
})
|
||||||
{
|
dirty.value = true
|
||||||
onError(err: any) {
|
} else {
|
||||||
toast.warning(__(err.messages?.[0] || err))
|
let courses = programCourses.data
|
||||||
|
courses.splice(targetIdx, 0, courses.splice(sourceIdx, 1)[0])
|
||||||
|
|
||||||
|
for (const [index, course] of courses.entries()) {
|
||||||
|
programCourses.setValue.submit(
|
||||||
|
{
|
||||||
|
name: course.name,
|
||||||
|
idx: index + 1,
|
||||||
},
|
},
|
||||||
}
|
{
|
||||||
)
|
onError(err: any) {
|
||||||
await wait(100)
|
toast.warning(__(err.messages?.[0] || err))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await wait(100)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms))
|
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms))
|
||||||
|
|
||||||
const remove = async (
|
const remove = (
|
||||||
selections: string[],
|
selections: string[],
|
||||||
unselectAll: () => void,
|
unselectAll: () => void,
|
||||||
type: string
|
type: string
|
||||||
) => {
|
) => {
|
||||||
selections = Array.from(selections)
|
const selectionsArray = Array.from(selections)
|
||||||
for (const selection of selections) {
|
if (type === 'courses') {
|
||||||
if (type == 'courses') {
|
program.value.program_courses = program.value.program_courses.filter(
|
||||||
await programCourses.delete.submit(selection)
|
(c: any) => !selectionsArray.includes(c.name || c.course)
|
||||||
await updateCounts('course', 'remove')
|
)
|
||||||
} else {
|
} else {
|
||||||
await programMembers.delete.submit(selection)
|
program.value.program_members = program.value.program_members.filter(
|
||||||
await updateCounts('member', 'remove')
|
(m: any) => !selectionsArray.includes(m.name || m.member)
|
||||||
}
|
)
|
||||||
await programs.value.reload()
|
|
||||||
await wait(100)
|
|
||||||
}
|
}
|
||||||
|
dirty.value = true
|
||||||
unselectAll()
|
unselectAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
const deleteProgram = (close: () => void) => {
|
const deleteProgram = (close: () => void) => {
|
||||||
if (props.programName == 'new') return
|
if (props.programName == 'new') return
|
||||||
programs.value?.delete.submit(props.programName, {
|
$dialog({
|
||||||
onSuccess() {
|
title: __('Delete Program'),
|
||||||
toast.success(__('Program deleted successfully'))
|
message: __(
|
||||||
close()
|
'Are you sure you want to delete this program? This action cannot be undone.'
|
||||||
},
|
),
|
||||||
onError(err: any) {
|
actions: [
|
||||||
toast.warning(__(err.messages?.[0] || err))
|
{
|
||||||
},
|
label: __('Delete'),
|
||||||
|
theme: 'red',
|
||||||
|
variant: 'solid',
|
||||||
|
onClick(closeDialog) {
|
||||||
|
programs.value?.delete.submit(props.programName, {
|
||||||
|
onSuccess() {
|
||||||
|
toast.success(__('Program deleted successfully'))
|
||||||
|
close()
|
||||||
|
closeDialog()
|
||||||
|
},
|
||||||
|
onError(err: any) {
|
||||||
|
toast.warning(__(err.messages?.[0] || err))
|
||||||
|
closeDialog()
|
||||||
|
},
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,7 +589,7 @@ const courseColumns = computed(() => {
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: 'Title',
|
label: 'Title',
|
||||||
key: 'course_title',
|
key: props.programName === 'new' ? 'course' : 'course_title',
|
||||||
width: 1,
|
width: 1,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -31,11 +31,11 @@
|
|||||||
categoryColumn: 'category',
|
categoryColumn: 'category',
|
||||||
valueColumn: 'count',
|
valueColumn: 'count',
|
||||||
colors: [
|
colors: [
|
||||||
theme.colors.red['400'],
|
getColor('red', 400),
|
||||||
theme.colors.amber['400'],
|
getColor('amber', 400),
|
||||||
theme.colors.pink['400'],
|
getColor('pink', 400),
|
||||||
theme.colors.blue['400'],
|
getColor('blue', 400),
|
||||||
theme.colors.green['400'],
|
getColor('green', 400),
|
||||||
],
|
],
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
@@ -74,7 +74,7 @@ import {
|
|||||||
} from 'frappe-ui'
|
} from 'frappe-ui'
|
||||||
import type { ProgramMember } from '@/types'
|
import type { ProgramMember } from '@/types'
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { theme } from '@/utils/theme'
|
import { getColor } from '@/utils'
|
||||||
|
|
||||||
const show = defineModel<boolean>({ default: false })
|
const show = defineModel<boolean>({ default: false })
|
||||||
const searchFilter = ref<string | null>(null)
|
const searchFilter = ref<string | null>(null)
|
||||||
|
|||||||
@@ -248,6 +248,23 @@ const routes = [
|
|||||||
name: 'Search',
|
name: 'Search',
|
||||||
component: () => import('@/pages/Search/Search.vue'),
|
component: () => import('@/pages/Search/Search.vue'),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/data-import',
|
||||||
|
name: 'DataImportList',
|
||||||
|
component: () => import('@/pages/DataImport.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/data-import/doctype/:doctype',
|
||||||
|
name: 'NewDataImport',
|
||||||
|
component: () => import('@/pages/DataImport.vue'),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/data-import/:importName',
|
||||||
|
name: 'DataImport',
|
||||||
|
component: () => import('@/pages/DataImport.vue'),
|
||||||
|
props: true,
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
let router = createRouter({
|
let router = createRouter({
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { io } from 'socket.io-client'
|
import { io } from 'socket.io-client'
|
||||||
import { socketio_port } from '../../../../sites/common_site_config.json'
|
import { socketio_port } from '../../../../sites/common_site_config.json'
|
||||||
import { getCachedListResource } from 'frappe-ui/src/resources/listResource'
|
|
||||||
import { getCachedResource } from 'frappe-ui/src/resources/resources'
|
|
||||||
|
|
||||||
export function initSocket() {
|
export function initSocket() {
|
||||||
let host = window.location.hostname
|
let host = window.location.hostname
|
||||||
@@ -14,15 +12,5 @@ export function initSocket() {
|
|||||||
withCredentials: true,
|
withCredentials: true,
|
||||||
reconnectionAttempts: 5,
|
reconnectionAttempts: 5,
|
||||||
})
|
})
|
||||||
socket.on('refetch_resource', (data) => {
|
|
||||||
if (data.cache_key) {
|
|
||||||
let resource =
|
|
||||||
getCachedResource(data.cache_key) ||
|
|
||||||
getCachedListResource(data.cache_key)
|
|
||||||
if (resource) {
|
|
||||||
resource.reload()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return socket
|
return socket
|
||||||
}
|
}
|
||||||
|
|||||||
499
frontend/src/utils/frappe-ui-colors.json
Normal file
499
frontend/src/utils/frappe-ui-colors.json
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
{
|
||||||
|
"lightMode": {
|
||||||
|
"gray": {
|
||||||
|
"50": "#F8F8F8",
|
||||||
|
"100": "#F3F3F3",
|
||||||
|
"200": "#EDEDED",
|
||||||
|
"300": "#E2E2E2",
|
||||||
|
"400": "#C7C7C7",
|
||||||
|
"500": "#999999",
|
||||||
|
"600": "#7C7C7C",
|
||||||
|
"700": "#525252",
|
||||||
|
"800": "#383838",
|
||||||
|
"900": "#171717"
|
||||||
|
},
|
||||||
|
"blue": {
|
||||||
|
"50": "#F2F9FF",
|
||||||
|
"100": "#E6F4FF",
|
||||||
|
"200": "#C8E6FF",
|
||||||
|
"300": "#A7D7FD",
|
||||||
|
"400": "#73BBF6",
|
||||||
|
"500": "#0289F7",
|
||||||
|
"600": "#007BE0",
|
||||||
|
"700": "#0070CC",
|
||||||
|
"800": "#005CA3",
|
||||||
|
"900": "#004880"
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"50": "#F2FDF4",
|
||||||
|
"100": "#E4FAEB",
|
||||||
|
"200": "#C3F9D3",
|
||||||
|
"300": "#A6EFC0",
|
||||||
|
"400": "#86E0A8",
|
||||||
|
"500": "#46B37E",
|
||||||
|
"600": "#278F5E",
|
||||||
|
"700": "#137949",
|
||||||
|
"800": "#075E35",
|
||||||
|
"900": "#173B2C"
|
||||||
|
},
|
||||||
|
"red": {
|
||||||
|
"50": "#FFF7F7",
|
||||||
|
"100": "#FFE7E7",
|
||||||
|
"200": "#FFD8D8",
|
||||||
|
"300": "#FDC2C2",
|
||||||
|
"400": "#F79596",
|
||||||
|
"500": "#E03636",
|
||||||
|
"600": "#CC2929",
|
||||||
|
"700": "#B52A2A",
|
||||||
|
"800": "#941F1F",
|
||||||
|
"900": "#6B1515"
|
||||||
|
},
|
||||||
|
"amber": {
|
||||||
|
"50": "#FDFAED",
|
||||||
|
"100": "#FFF7D3",
|
||||||
|
"200": "#FEEDA9",
|
||||||
|
"300": "#FBDB73",
|
||||||
|
"400": "#FBCC55",
|
||||||
|
"500": "#E79913",
|
||||||
|
"600": "#DB7706",
|
||||||
|
"700": "#B35309",
|
||||||
|
"800": "#91400D",
|
||||||
|
"900": "#763813"
|
||||||
|
},
|
||||||
|
"orange": {
|
||||||
|
"50": "#FFF9F5",
|
||||||
|
"100": "#FFEFE4",
|
||||||
|
"200": "#FFDEC5",
|
||||||
|
"300": "#FFCBA3",
|
||||||
|
"400": "#F4B07F",
|
||||||
|
"500": "#E86C13",
|
||||||
|
"600": "#D45A08",
|
||||||
|
"700": "#BD3E0C",
|
||||||
|
"800": "#9E3513",
|
||||||
|
"900": "#6B2711"
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"50": "#FFFCEF",
|
||||||
|
"100": "#FFF7D3",
|
||||||
|
"200": "#F7E9A8",
|
||||||
|
"300": "#F5E171",
|
||||||
|
"400": "#F2D14B",
|
||||||
|
"500": "#EDBA13",
|
||||||
|
"600": "#D1930D",
|
||||||
|
"700": "#AB6E05",
|
||||||
|
"800": "#8C5600",
|
||||||
|
"900": "#733F12"
|
||||||
|
},
|
||||||
|
"teal": {
|
||||||
|
"50": "#F0FDFA",
|
||||||
|
"100": "#E6F7F4",
|
||||||
|
"200": "#BAE8E1",
|
||||||
|
"300": "#97DED4",
|
||||||
|
"400": "#73D1C4",
|
||||||
|
"500": "#36BAAD",
|
||||||
|
"600": "#0B9E92",
|
||||||
|
"700": "#0F736B",
|
||||||
|
"800": "#115C57",
|
||||||
|
"900": "#114541"
|
||||||
|
},
|
||||||
|
"cyan": {
|
||||||
|
"50": "#F5FBFC",
|
||||||
|
"100": "#DDF7FF",
|
||||||
|
"200": "#B3E8F7",
|
||||||
|
"300": "#99E2F8",
|
||||||
|
"400": "#72D5F3",
|
||||||
|
"500": "#3BBDE5",
|
||||||
|
"600": "#32A4C7",
|
||||||
|
"700": "#267A94",
|
||||||
|
"800": "#125C73",
|
||||||
|
"900": "#164759"
|
||||||
|
},
|
||||||
|
"purple": {
|
||||||
|
"50": "#FDFAFF",
|
||||||
|
"100": "#F6E9FF",
|
||||||
|
"200": "#ECD3FF",
|
||||||
|
"300": "#E2B9FC",
|
||||||
|
"400": "#CFA1F2",
|
||||||
|
"500": "#9C45E3",
|
||||||
|
"600": "#8642C2",
|
||||||
|
"700": "#6E399D",
|
||||||
|
"800": "#5C2F83",
|
||||||
|
"900": "#401863"
|
||||||
|
},
|
||||||
|
"pink": {
|
||||||
|
"50": "#FFF7FC",
|
||||||
|
"100": "#FDE8F5",
|
||||||
|
"200": "#FFD5F0",
|
||||||
|
"300": "#F9B9E0",
|
||||||
|
"400": "#F6A7D6",
|
||||||
|
"500": "#E34AA6",
|
||||||
|
"600": "#CF3A96",
|
||||||
|
"700": "#9C2671",
|
||||||
|
"800": "#801458",
|
||||||
|
"900": "#570F3E"
|
||||||
|
},
|
||||||
|
"violet": {
|
||||||
|
"50": "#FBFAFF",
|
||||||
|
"100": "#F0EBFF",
|
||||||
|
"200": "#DBD5FF",
|
||||||
|
"300": "#C9BAFB",
|
||||||
|
"400": "#B3A1F5",
|
||||||
|
"500": "#6846E3",
|
||||||
|
"600": "#5F46C7",
|
||||||
|
"700": "#4F3DA1",
|
||||||
|
"800": "#392980",
|
||||||
|
"900": "#251959"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"darkMode": {
|
||||||
|
"gray": {
|
||||||
|
"50": "#F8F8F8",
|
||||||
|
"100": "#D4D4D4",
|
||||||
|
"200": "#AFAFAF",
|
||||||
|
"250": "#999999",
|
||||||
|
"300": "#808080",
|
||||||
|
"400": "#717171",
|
||||||
|
"500": "#424242",
|
||||||
|
"600": "#343434",
|
||||||
|
"650": "#2B2B2B",
|
||||||
|
"700": "#232323",
|
||||||
|
"800": "#1C1C1C",
|
||||||
|
"900": "#0F0F0F"
|
||||||
|
},
|
||||||
|
"blue": {
|
||||||
|
"50": "#C9E0F5",
|
||||||
|
"100": "#ADD2F5",
|
||||||
|
"200": "#8CC1EC",
|
||||||
|
"300": "#5AAEF2",
|
||||||
|
"400": "#3294E3",
|
||||||
|
"500": "#1580D8",
|
||||||
|
"600": "#155999",
|
||||||
|
"700": "#063D71",
|
||||||
|
"800": "#052B53",
|
||||||
|
"900": "#0E2037",
|
||||||
|
"900-80": "#0E2037CC"
|
||||||
|
},
|
||||||
|
"green": {
|
||||||
|
"50": "#C8F3DE",
|
||||||
|
"100": "#9BE6C1",
|
||||||
|
"200": "#78D7A9",
|
||||||
|
"300": "#58C08E",
|
||||||
|
"400": "#1BA964",
|
||||||
|
"500": "#0A9752",
|
||||||
|
"600": "#0F814A",
|
||||||
|
"700": "#035831",
|
||||||
|
"800": "#0A3F27",
|
||||||
|
"900": "#0B2E1C"
|
||||||
|
},
|
||||||
|
"red": {
|
||||||
|
"50": "#FFC1C1",
|
||||||
|
"100": "#FF9595",
|
||||||
|
"200": "#FC7474",
|
||||||
|
"300": "#EB4D52",
|
||||||
|
"400": "#E43838",
|
||||||
|
"500": "#C12020",
|
||||||
|
"600": "#901818",
|
||||||
|
"700": "#681916",
|
||||||
|
"800": "#521515",
|
||||||
|
"900": "#361515",
|
||||||
|
"800-90": "#521515E6",
|
||||||
|
"900-90": "#361515E6"
|
||||||
|
},
|
||||||
|
"amber": {
|
||||||
|
"50": "#F9E8A5",
|
||||||
|
"100": "#F8D16E",
|
||||||
|
"200": "#F0BA31",
|
||||||
|
"300": "#E79913",
|
||||||
|
"400": "#E37D00",
|
||||||
|
"500": "#CB6D10",
|
||||||
|
"600": "#824108",
|
||||||
|
"700": "#603007",
|
||||||
|
"800": "#4B2606",
|
||||||
|
"900": "#371E06"
|
||||||
|
},
|
||||||
|
"orange": {
|
||||||
|
"50": "#FFCDAD",
|
||||||
|
"100": "#FFA873",
|
||||||
|
"200": "#FA8A40",
|
||||||
|
"300": "#DE6D1B",
|
||||||
|
"400": "#C45A0E",
|
||||||
|
"500": "#984509",
|
||||||
|
"600": "#823906",
|
||||||
|
"700": "#683108",
|
||||||
|
"800": "#532707",
|
||||||
|
"900": "#401F07",
|
||||||
|
"900-80": "#401F07CC"
|
||||||
|
},
|
||||||
|
"yellow": {
|
||||||
|
"50": "#FFE89D",
|
||||||
|
"100": "#F8D76A",
|
||||||
|
"200": "#ECC02E",
|
||||||
|
"300": "#DAAE15",
|
||||||
|
"400": "#C69C12",
|
||||||
|
"500": "#9C7A0A",
|
||||||
|
"600": "#705606",
|
||||||
|
"700": "#5B4605",
|
||||||
|
"800": "#3F3004",
|
||||||
|
"900": "#322604"
|
||||||
|
},
|
||||||
|
"teal": {
|
||||||
|
"50": "#93F2E8",
|
||||||
|
"100": "#6EE7DB",
|
||||||
|
"200": "#52DACC",
|
||||||
|
"300": "#3DC6B8",
|
||||||
|
"400": "#219C8F",
|
||||||
|
"500": "#1B7169",
|
||||||
|
"600": "#13564F",
|
||||||
|
"700": "#0C423C",
|
||||||
|
"800": "#0B3A35",
|
||||||
|
"900": "#0A2D29"
|
||||||
|
},
|
||||||
|
"cyan": {
|
||||||
|
"50": "#D0F0FA",
|
||||||
|
"100": "#A0E6F7",
|
||||||
|
"200": "#68D3F3",
|
||||||
|
"300": "#3CB8DC",
|
||||||
|
"400": "#2B8DAB",
|
||||||
|
"500": "#23728B",
|
||||||
|
"600": "#155266",
|
||||||
|
"700": "#0E3B49",
|
||||||
|
"800": "#0D2B36",
|
||||||
|
"900": "#0B252D"
|
||||||
|
},
|
||||||
|
"purple": {
|
||||||
|
"50": "#E5C6FB",
|
||||||
|
"100": "#D9AFF5",
|
||||||
|
"200": "#C993EF",
|
||||||
|
"300": "#B168E8",
|
||||||
|
"400": "#984BD8",
|
||||||
|
"500": "#7A2DB9",
|
||||||
|
"600": "#591F89",
|
||||||
|
"700": "#47176E",
|
||||||
|
"800": "#391457",
|
||||||
|
"900": "#2E1146"
|
||||||
|
},
|
||||||
|
"pink": {
|
||||||
|
"50": "#F6C5DE",
|
||||||
|
"100": "#F69AD1",
|
||||||
|
"200": "#ED77BE",
|
||||||
|
"300": "#E359AB",
|
||||||
|
"400": "#CB4394",
|
||||||
|
"500": "#AC377D",
|
||||||
|
"600": "#822A5F",
|
||||||
|
"700": "#68204B",
|
||||||
|
"800": "#601D46",
|
||||||
|
"900": "#471432",
|
||||||
|
"900-80": "#471432CC"
|
||||||
|
},
|
||||||
|
"violet": {
|
||||||
|
"50": "#DACBF7",
|
||||||
|
"100": "#C4AFEE",
|
||||||
|
"200": "#B398EF",
|
||||||
|
"300": "#9D7CEA",
|
||||||
|
"400": "#8867E8",
|
||||||
|
"500": "#5C3FC2",
|
||||||
|
"600": "#4639A6",
|
||||||
|
"700": "#332978",
|
||||||
|
"800": "#281E5D",
|
||||||
|
"900": "#221C42"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"overlay": {
|
||||||
|
"white": {
|
||||||
|
"50": "#FFFFFF1A",
|
||||||
|
"100": "#FFFFFF2E",
|
||||||
|
"200": "#FFFFFF45",
|
||||||
|
"300": "#FFFFFF5C",
|
||||||
|
"400": "#FFFFFF73",
|
||||||
|
"500": "#FFFFFF8A",
|
||||||
|
"600": "#FFFFFFA1",
|
||||||
|
"700": "#FFFFFFB8",
|
||||||
|
"800": "#FFFFFFCF",
|
||||||
|
"900": "#FFFFFFE6"
|
||||||
|
},
|
||||||
|
"black": {
|
||||||
|
"50": "#00000017",
|
||||||
|
"100": "#0000002E",
|
||||||
|
"200": "#00000045",
|
||||||
|
"300": "#0000005C",
|
||||||
|
"400": "#00000073",
|
||||||
|
"500": "#0000008A",
|
||||||
|
"600": "#000000A1",
|
||||||
|
"700": "#000000B8",
|
||||||
|
"800": "#000000CF",
|
||||||
|
"900": "#000000E6"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"neutral": {
|
||||||
|
"white": "#FFFFFF",
|
||||||
|
"black": "#000000"
|
||||||
|
},
|
||||||
|
"themedVariables": {
|
||||||
|
"light": {
|
||||||
|
"outline": {
|
||||||
|
"white": "neutral/white",
|
||||||
|
"gray-1": "lightMode/gray/200",
|
||||||
|
"gray-2": "lightMode/gray/300",
|
||||||
|
"gray-3": "lightMode/gray/400",
|
||||||
|
"gray-4": "lightMode/gray/500",
|
||||||
|
"gray-5": "lightMode/gray/800",
|
||||||
|
"red-1": "lightMode/red/300",
|
||||||
|
"red-2": "lightMode/red/400",
|
||||||
|
"red-3": "lightMode/red/500",
|
||||||
|
"green-1": "lightMode/green/300",
|
||||||
|
"green-2": "lightMode/green/400",
|
||||||
|
"amber-1": "lightMode/amber/300",
|
||||||
|
"amber-2": "lightMode/amber/400",
|
||||||
|
"blue-1": "lightMode/blue/300",
|
||||||
|
"orange-1": "lightMode/orange/400",
|
||||||
|
"gray-modals": "lightMode/gray/200"
|
||||||
|
},
|
||||||
|
"surface": {
|
||||||
|
"white": "neutral/white",
|
||||||
|
"gray-1": "lightMode/gray/50",
|
||||||
|
"gray-2": "lightMode/gray/100",
|
||||||
|
"gray-3": "lightMode/gray/200",
|
||||||
|
"gray-4": "lightMode/gray/300",
|
||||||
|
"gray-5": "lightMode/gray/700",
|
||||||
|
"gray-6": "lightMode/gray/800",
|
||||||
|
"gray-7": "lightMode/gray/900",
|
||||||
|
"red-1": "lightMode/red/50",
|
||||||
|
"red-2": "lightMode/red/100",
|
||||||
|
"red-3": "lightMode/red/200",
|
||||||
|
"red-4": "lightMode/red/300",
|
||||||
|
"red-5": "lightMode/red/600",
|
||||||
|
"red-6": "lightMode/red/700",
|
||||||
|
"red-7": "lightMode/red/800",
|
||||||
|
"green-1": "lightMode/green/50",
|
||||||
|
"green-2": "lightMode/green/100",
|
||||||
|
"green-3": "lightMode/green/600",
|
||||||
|
"amber-1": "lightMode/amber/50",
|
||||||
|
"amber-2": "lightMode/amber/100",
|
||||||
|
"amber-3": "lightMode/amber/600",
|
||||||
|
"blue-1": "lightMode/blue/50",
|
||||||
|
"blue-2": "lightMode/blue/100",
|
||||||
|
"blue-3": "lightMode/blue/600",
|
||||||
|
"orange-1": "lightMode/orange/100",
|
||||||
|
"violet-1": "lightMode/violet/100",
|
||||||
|
"cyan-1": "lightMode/cyan/100",
|
||||||
|
"pink-1": "lightMode/pink/100",
|
||||||
|
"menu-bar": "lightMode/gray/50",
|
||||||
|
"cards": "neutral/white",
|
||||||
|
"modal": "neutral/white",
|
||||||
|
"selected": "neutral/white"
|
||||||
|
},
|
||||||
|
"ink": {
|
||||||
|
"white": "neutral/white",
|
||||||
|
"gray-1": "lightMode/gray/200",
|
||||||
|
"gray-2": "lightMode/gray/300",
|
||||||
|
"gray-3": "lightMode/gray/400",
|
||||||
|
"gray-4": "lightMode/gray/500",
|
||||||
|
"gray-5": "lightMode/gray/600",
|
||||||
|
"gray-6": "lightMode/gray/700",
|
||||||
|
"gray-7": "lightMode/gray/700",
|
||||||
|
"gray-8": "lightMode/gray/800",
|
||||||
|
"gray-9": "lightMode/gray/900",
|
||||||
|
"red-1": "lightMode/red/50",
|
||||||
|
"red-2": "lightMode/red/400",
|
||||||
|
"red-3": "lightMode/red/500",
|
||||||
|
"red-4": "lightMode/red/600",
|
||||||
|
"green-1": "lightMode/green/50",
|
||||||
|
"green-2": "lightMode/green/500",
|
||||||
|
"green-3": "lightMode/green/600",
|
||||||
|
"amber-1": "lightMode/amber/50",
|
||||||
|
"amber-2": "lightMode/amber/500",
|
||||||
|
"amber-3": "lightMode/amber/600",
|
||||||
|
"blue-1": "lightMode/blue/50",
|
||||||
|
"blue-2": "lightMode/blue/500",
|
||||||
|
"blue-3": "lightMode/blue/600",
|
||||||
|
"cyan-1": "lightMode/cyan/500",
|
||||||
|
"pink-1": "lightMode/pink/500",
|
||||||
|
"violet-1": "lightMode/violet/500",
|
||||||
|
"blue-link": "lightMode/blue/400"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"outline": {
|
||||||
|
"white": "darkMode/gray/800",
|
||||||
|
"gray-1": "darkMode/gray/700",
|
||||||
|
"gray-2": "darkMode/gray/600",
|
||||||
|
"gray-3": "darkMode/gray/500",
|
||||||
|
"gray-4": "darkMode/gray/300",
|
||||||
|
"gray-5": "lightMode/gray/200",
|
||||||
|
"red-1": "darkMode/red/800",
|
||||||
|
"red-2": "darkMode/red/700",
|
||||||
|
"red-3": "darkMode/red/600",
|
||||||
|
"green-1": "darkMode/green/800",
|
||||||
|
"green-2": "darkMode/green/700",
|
||||||
|
"amber-1": "darkMode/amber/800",
|
||||||
|
"amber-2": "darkMode/amber/700",
|
||||||
|
"blue-1": "darkMode/blue/800",
|
||||||
|
"orange-1": "darkMode/orange/700",
|
||||||
|
"gray-modals": "darkMode/gray/600"
|
||||||
|
},
|
||||||
|
"surface": {
|
||||||
|
"white": "darkMode/gray/900",
|
||||||
|
"gray-1": "darkMode/gray/700",
|
||||||
|
"gray-2": "darkMode/gray/650",
|
||||||
|
"gray-3": "darkMode/gray/600",
|
||||||
|
"gray-4": "darkMode/gray/500",
|
||||||
|
"gray-5": "darkMode/gray/200",
|
||||||
|
"gray-6": "darkMode/gray/100",
|
||||||
|
"gray-7": "darkMode/gray/50",
|
||||||
|
"red-1": "darkMode/red/900",
|
||||||
|
"red-2": "darkMode/red/900-90",
|
||||||
|
"red-3": "darkMode/red/800-90",
|
||||||
|
"red-4": "darkMode/red/700",
|
||||||
|
"red-5": "darkMode/red/400",
|
||||||
|
"red-6": "darkMode/red/500",
|
||||||
|
"red-7": "darkMode/red/600",
|
||||||
|
"green-1": "darkMode/green/900",
|
||||||
|
"green-2": "darkMode/green/800",
|
||||||
|
"green-3": "darkMode/green/400",
|
||||||
|
"amber-1": "darkMode/amber/900",
|
||||||
|
"amber-2": "darkMode/amber/800",
|
||||||
|
"amber-3": "darkMode/amber/400",
|
||||||
|
"blue-1": "darkMode/blue/900",
|
||||||
|
"blue-2": "darkMode/blue/800",
|
||||||
|
"blue-3": "darkMode/blue/400",
|
||||||
|
"orange-1": "darkMode/orange/900-80",
|
||||||
|
"violet-1": "darkMode/violet/900",
|
||||||
|
"cyan-1": "darkMode/cyan/900",
|
||||||
|
"pink-1": "darkMode/pink/900-80",
|
||||||
|
"menu-bar": "darkMode/gray/900",
|
||||||
|
"cards": "darkMode/gray/800",
|
||||||
|
"modal": "darkMode/gray/700",
|
||||||
|
"selected": "darkMode/gray/500"
|
||||||
|
},
|
||||||
|
"ink": {
|
||||||
|
"white": "darkMode/gray/900",
|
||||||
|
"gray-1": "darkMode/gray/700",
|
||||||
|
"gray-2": "darkMode/gray/500",
|
||||||
|
"gray-3": "darkMode/gray/400",
|
||||||
|
"gray-4": "darkMode/gray/400",
|
||||||
|
"gray-5": "darkMode/gray/300",
|
||||||
|
"gray-6": "darkMode/gray/250",
|
||||||
|
"gray-7": "darkMode/gray/200",
|
||||||
|
"gray-8": "darkMode/gray/100",
|
||||||
|
"gray-9": "darkMode/gray/50",
|
||||||
|
"red-1": "neutral/white",
|
||||||
|
"red-2": "darkMode/red/700",
|
||||||
|
"red-3": "darkMode/red/400",
|
||||||
|
"red-4": "darkMode/red/200",
|
||||||
|
"green-1": "neutral/white",
|
||||||
|
"green-2": "darkMode/green/400",
|
||||||
|
"green-3": "darkMode/green/300",
|
||||||
|
"amber-1": "neutral/white",
|
||||||
|
"amber-2": "darkMode/amber/400",
|
||||||
|
"amber-3": "darkMode/amber/300",
|
||||||
|
"blue-1": "neutral/white",
|
||||||
|
"blue-2": "darkMode/blue/400",
|
||||||
|
"blue-3": "darkMode/blue/300",
|
||||||
|
"cyan-1": "darkMode/cyan/300",
|
||||||
|
"pink-1": "darkMode/pink/300",
|
||||||
|
"violet-1": "darkMode/violet/300",
|
||||||
|
"blue-link": "darkMode/blue/500"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { call, toast } from 'frappe-ui'
|
import { call, toast } from 'frappe-ui'
|
||||||
import { useTimeAgo } from '@vueuse/core'
|
import { useTimeAgo } from '@vueuse/core'
|
||||||
import { theme } from '@/utils/theme'
|
import colorsJSON from '@/utils/frappe-ui-colors.json'
|
||||||
import { Quiz } from '@/utils/quiz'
|
import { Quiz } from '@/utils/quiz'
|
||||||
import { Program } from '@/utils/program'
|
import { Program } from '@/utils/program'
|
||||||
import { Assignment } from '@/utils/assignment'
|
import { Assignment } from '@/utils/assignment'
|
||||||
@@ -680,7 +680,7 @@ export const getMetaInfo = (type, route, meta) => {
|
|||||||
|
|
||||||
export const updateMetaInfo = (type, route, meta) => {
|
export const updateMetaInfo = (type, route, meta) => {
|
||||||
call('lms.lms.api.update_meta_info', {
|
call('lms.lms.api.update_meta_info', {
|
||||||
type: type,
|
meta_type: type,
|
||||||
route: route,
|
route: route,
|
||||||
meta_tags: [
|
meta_tags: [
|
||||||
{ key: 'description', value: meta.description },
|
{ key: 'description', value: meta.description },
|
||||||
@@ -734,10 +734,10 @@ const createHighlightSpan = (color, name, scrollIntoView) => {
|
|||||||
const span = document.createElement('span')
|
const span = document.createElement('span')
|
||||||
span.className = 'highlighted-text'
|
span.className = 'highlighted-text'
|
||||||
if (scrollIntoView) {
|
if (scrollIntoView) {
|
||||||
span.style.border = `2px solid ${theme.backgroundColor[color][400]}`
|
span.style.border = `2px solid ${getColor(color, 400)}`
|
||||||
span.style.borderRadius = '4px'
|
span.style.borderRadius = '4px'
|
||||||
} else {
|
} else {
|
||||||
span.style.backgroundColor = theme.backgroundColor[color][200]
|
span.style.backgroundColor = getColor(color, 200)
|
||||||
}
|
}
|
||||||
span.dataset.name = name
|
span.dataset.name = name
|
||||||
return span
|
return span
|
||||||
@@ -810,3 +810,9 @@ export const decodeEntities = (encodedString) => {
|
|||||||
textarea.innerHTML = encodedString
|
textarea.innerHTML = encodedString
|
||||||
return textarea.value
|
return textarea.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getColor = (color, shade) => {
|
||||||
|
let theme =
|
||||||
|
localStorage.getItem('theme') == 'light' ? 'lightMode' : 'darkMode'
|
||||||
|
return colorsJSON[theme][color][shade]
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ export class Markdown {
|
|||||||
this.api = api
|
this.api = api
|
||||||
this.data = data || {}
|
this.data = data || {}
|
||||||
this.config = config || {}
|
this.config = config || {}
|
||||||
this.text = data.text || ''
|
|
||||||
this.readOnly = readOnly
|
this.readOnly = readOnly
|
||||||
|
this.text = data.text || ''
|
||||||
this.placeholder = __("Type '/' for commands or select text to format")
|
this.placeholder = __("Type '/' for commands or select text to format")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,65 +30,28 @@ export class Markdown {
|
|||||||
|
|
||||||
const div = document.createElement('div')
|
const div = document.createElement('div')
|
||||||
app.mount(div)
|
app.mount(div)
|
||||||
return {
|
return { title: '', icon: div.innerHTML }
|
||||||
title: '',
|
|
||||||
icon: div.innerHTML,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onPaste(event) {
|
|
||||||
const data = {
|
|
||||||
text: event.detail.data.innerHTML,
|
|
||||||
}
|
|
||||||
|
|
||||||
this.data = data
|
|
||||||
window.requestAnimationFrame(() => {
|
|
||||||
if (!this.wrapper) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.wrapper.innerHTML = this.data.text || ''
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static get pasteConfig() {
|
static get pasteConfig() {
|
||||||
return {
|
return { tags: ['P'] }
|
||||||
tags: ['P'],
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.wrapper = document.createElement('div')
|
this.wrapper = document.createElement('div')
|
||||||
this.wrapper.classList.add('cdx-block', 'ce-paragraph')
|
this.wrapper.classList.add('cdx-block', 'ce-paragraph')
|
||||||
|
this.wrapper.contentEditable = !this.readOnly
|
||||||
|
this.wrapper.dataset.placeholder = this.placeholder
|
||||||
this.wrapper.innerHTML = this.text
|
this.wrapper.innerHTML = this.text
|
||||||
|
|
||||||
if (!this.readOnly) {
|
if (!this.readOnly) {
|
||||||
this.wrapper.contentEditable = true
|
|
||||||
this.wrapper.innerHTML = this.text
|
|
||||||
|
|
||||||
this.wrapper.addEventListener('focus', () =>
|
this.wrapper.addEventListener('focus', () =>
|
||||||
this._togglePlaceholder()
|
this._togglePlaceholder()
|
||||||
)
|
)
|
||||||
this.wrapper.addEventListener('blur', () =>
|
this.wrapper.addEventListener('blur', () =>
|
||||||
this._togglePlaceholder()
|
this._togglePlaceholder()
|
||||||
)
|
)
|
||||||
|
this.wrapper.addEventListener('keydown', (e) => this._onKeyDown(e))
|
||||||
this.wrapper.addEventListener('input', (event) => {
|
|
||||||
this._togglePlaceholder()
|
|
||||||
let value = event.target.textContent
|
|
||||||
if (event.keyCode === 32 && value.startsWith('#')) {
|
|
||||||
this.convertToHeader(event, value)
|
|
||||||
} else if (event.keyCode == 189) {
|
|
||||||
this.convertBlock('list', {
|
|
||||||
style: 'unordered',
|
|
||||||
})
|
|
||||||
} else if (/^[a-zA-Z]/.test(event.key)) {
|
|
||||||
this.convertBlock('paragraph', {
|
|
||||||
text: value,
|
|
||||||
})
|
|
||||||
} else if (event.keyCode === 13 || event.keyCode === 190) {
|
|
||||||
this.parseContent(event)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.wrapper
|
return this.wrapper
|
||||||
@@ -99,10 +62,9 @@ export class Markdown {
|
|||||||
'.cdx-block.ce-paragraph[data-placeholder]'
|
'.cdx-block.ce-paragraph[data-placeholder]'
|
||||||
)
|
)
|
||||||
blocks.forEach((block) => {
|
blocks.forEach((block) => {
|
||||||
if (block !== this.wrapper) {
|
if (block !== this.wrapper) delete block.dataset.placeholder
|
||||||
delete block.dataset.placeholder
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if (this.wrapper.innerHTML.trim() === '') {
|
if (this.wrapper.innerHTML.trim() === '') {
|
||||||
this.wrapper.dataset.placeholder = this.placeholder
|
this.wrapper.dataset.placeholder = this.placeholder
|
||||||
} else {
|
} else {
|
||||||
@@ -110,102 +72,107 @@ export class Markdown {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
convertToHeader(event, value) {
|
_onKeyDown(event) {
|
||||||
event.preventDefault()
|
const text = this.wrapper.textContent
|
||||||
if (['#', '##', '###', '####', '#####', '######'].includes(value)) {
|
|
||||||
let level = value.length
|
|
||||||
event.target.textContent = ''
|
|
||||||
this.convertBlock('header', {
|
|
||||||
level: level,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
parseContent(event) {
|
if (event.key === ' ' && /^#{1,6}$/.test(text)) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
let previousLine = this.wrapper.textContent
|
const level = text.length
|
||||||
if (event.keyCode === 190) {
|
|
||||||
previousLine = previousLine + '.'
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousLine && this.hasImage(previousLine)) {
|
|
||||||
this.wrapper.textContent = ''
|
this.wrapper.textContent = ''
|
||||||
this.convertBlock('image')
|
this._convertBlock('header', { level })
|
||||||
} else if (previousLine && this.hasLink(previousLine)) {
|
} else if (event.key === ' ' && text === '-') {
|
||||||
const { text, url } = this.extractLink(previousLine)
|
event.preventDefault()
|
||||||
const anchorTag = `<a href="${url}" target="_blank">${text}</a>`
|
this.wrapper.textContent = ''
|
||||||
this.convertBlock('paragraph', {
|
this._convertBlock('list', {
|
||||||
text: previousLine.replace(/\[.+?\]\(.+?\)/, anchorTag),
|
|
||||||
})
|
|
||||||
} else if (previousLine && previousLine.startsWith('- ')) {
|
|
||||||
this.convertBlock('list', {
|
|
||||||
style: 'unordered',
|
style: 'unordered',
|
||||||
items: [
|
items: [{ content: '' }],
|
||||||
{
|
|
||||||
content: previousLine.replace('- ', ''),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
})
|
||||||
} else if (previousLine && previousLine.startsWith('1.')) {
|
} else if (event.key === ' ' && /^1\.$/.test(text)) {
|
||||||
this.convertBlock('list', {
|
event.preventDefault()
|
||||||
style: 'ordered',
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
content: previousLine.replace('1.', ''),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
} else if (previousLine && this.canBeEmbed(previousLine)) {
|
|
||||||
this.wrapper.textContent = ''
|
this.wrapper.textContent = ''
|
||||||
this.convertBlock('embed', {
|
this._convertBlock('list', {
|
||||||
source: previousLine,
|
style: 'ordered',
|
||||||
|
items: [{ content: '' }],
|
||||||
})
|
})
|
||||||
} else {
|
} else if (this._isEmbed(text) && event.key === 'Enter') {
|
||||||
this.convertBlock('paragraph', {
|
event.preventDefault()
|
||||||
text: previousLine,
|
this.wrapper.textContent = ''
|
||||||
|
this._convertBlock('embed', { source: text })
|
||||||
|
} else if (event.key === 'Enter') {
|
||||||
|
setTimeout(() => this._checkMarkdownAfterEnter(), 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_checkMarkdownAfterEnter() {
|
||||||
|
const text = this.wrapper.textContent.trim()
|
||||||
|
|
||||||
|
if (this._isImage(text)) {
|
||||||
|
this._convertBlock('image', {
|
||||||
|
file: { url: this._extractImage(text).url },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async convertBlock(type, data, index = null) {
|
async _convertBlock(type, data) {
|
||||||
const currentIndex = this.api.blocks.getCurrentBlockIndex()
|
const currentIndex = this.api.blocks.getCurrentBlockIndex()
|
||||||
const currentBlock = this.api.blocks.getBlockByIndex(currentIndex)
|
const currentBlock = this.api.blocks.getBlockByIndex(currentIndex)
|
||||||
|
|
||||||
|
if (!currentBlock) return
|
||||||
|
|
||||||
await this.api.blocks.convert(currentBlock.id, type, data)
|
await this.api.blocks.convert(currentBlock.id, type, data)
|
||||||
this.api.caret.focus(true)
|
|
||||||
|
setTimeout(() => {
|
||||||
|
const newIndex = this.api.blocks.getCurrentBlockIndex()
|
||||||
|
const newBlock = this.api.blocks.getBlockByIndex(newIndex)
|
||||||
|
|
||||||
|
if (newBlock && newBlock.holder) {
|
||||||
|
const holder = newBlock.holder.querySelector(
|
||||||
|
'[contenteditable="true"]'
|
||||||
|
)
|
||||||
|
if (holder) {
|
||||||
|
holder.focus()
|
||||||
|
// Place caret at end
|
||||||
|
const range = document.createRange()
|
||||||
|
range.selectNodeContents(holder)
|
||||||
|
range.collapse(false)
|
||||||
|
const sel = window.getSelection()
|
||||||
|
sel.removeAllRanges()
|
||||||
|
sel.addRange(range)
|
||||||
|
} else {
|
||||||
|
this.api.caret.focus(true)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.api.caret.focus(true)
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
save(blockContent) {
|
save(blockContent) {
|
||||||
return {
|
return { text: blockContent.innerHTML }
|
||||||
text: blockContent.innerHTML,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
hasImage(line) {
|
_isImage(text) {
|
||||||
return /!\[.+?\]\(.+?\)/.test(line)
|
return /!\[.+?\]\(.+?\)/.test(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
extractImage(line) {
|
_extractImage(text) {
|
||||||
const match = line.match(/!\[(.+?)\]\((.+?)\)/)
|
const match = text.match(/!\[(.+?)\]\((.+?)\)/)
|
||||||
if (match) {
|
if (match) return { alt: match[1], url: match[2] }
|
||||||
return { alt: match[1], url: match[2] }
|
|
||||||
}
|
|
||||||
return { alt: '', url: '' }
|
return { alt: '', url: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
hasLink(line) {
|
_isLink(text) {
|
||||||
return /\[.+?\]\(.+?\)/.test(line)
|
return /\[.+?\]\(.+?\)/.test(text)
|
||||||
}
|
}
|
||||||
|
|
||||||
extractLink(line) {
|
_extractLink(text) {
|
||||||
const match = line.match(/\[(.+?)\]\((.+?)\)/)
|
const match = text.match(/\[(.+?)\]\((.+?)\)/)
|
||||||
if (match) {
|
if (match) return { text: match[1], url: match[2] }
|
||||||
return { text: match[1], url: match[2] }
|
|
||||||
}
|
|
||||||
return { text: '', url: '' }
|
return { text: '', url: '' }
|
||||||
}
|
}
|
||||||
|
|
||||||
canBeEmbed(line) {
|
_isEmbed(text) {
|
||||||
return /^https?:\/\/.+/.test(line.trim())
|
return /^https?:\/\/.+/.test(text.trim())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
import resolveConfig from 'tailwindcss/resolveConfig'
|
|
||||||
import tailwindConfig from 'tailwind.config.js'
|
|
||||||
|
|
||||||
export const config = resolveConfig(tailwindConfig)
|
|
||||||
export const theme = config.theme
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import frappeUIPreset from 'frappe-ui/src/tailwind/preset'
|
import frappeUIPreset from 'frappe-ui/tailwind'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
presets: [frappeUIPreset],
|
presets: [frappeUIPreset],
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import frappeui from 'frappe-ui/vite'
|
|||||||
import { VitePWA } from 'vite-plugin-pwa'
|
import { VitePWA } from 'vite-plugin-pwa'
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig(({ mode }) => ({
|
||||||
plugins: [
|
plugins: [
|
||||||
frappeui({
|
frappeui({
|
||||||
frappeProxy: true,
|
frappeProxy: true,
|
||||||
@@ -32,6 +32,18 @@ export default defineConfig({
|
|||||||
workbox: {
|
workbox: {
|
||||||
cleanupOutdatedCaches: true,
|
cleanupOutdatedCaches: true,
|
||||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024,
|
||||||
|
globDirectory: '/assets/lms/frontend',
|
||||||
|
globPatterns: ['**/*.{js,ts,css,html,png,svg}'],
|
||||||
|
runtimeCaching: [
|
||||||
|
{
|
||||||
|
urlPattern: ({ request }) =>
|
||||||
|
request.destination === 'document',
|
||||||
|
handler: 'NetworkFirst',
|
||||||
|
options: {
|
||||||
|
cacheName: 'html-cache',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
manifest: false,
|
manifest: false,
|
||||||
}),
|
}),
|
||||||
@@ -43,18 +55,16 @@ export default defineConfig({
|
|||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, 'src'),
|
'@': path.resolve(__dirname, 'src'),
|
||||||
'tailwind.config.js': path.resolve(__dirname, 'tailwind.config.js'),
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: [
|
include: [
|
||||||
'feather-icons',
|
'feather-icons',
|
||||||
'showdown',
|
|
||||||
'engine.io-client',
|
'engine.io-client',
|
||||||
'tailwind.config.js',
|
|
||||||
'interactjs',
|
'interactjs',
|
||||||
'highlight.js',
|
'highlight.js',
|
||||||
'plyr',
|
'plyr',
|
||||||
],
|
],
|
||||||
|
exclude: mode === 'production' ? [] : ['frappe-ui'],
|
||||||
},
|
},
|
||||||
})
|
}))
|
||||||
|
|||||||
1864
frontend/yarn.lock
1864
frontend/yarn.lock
File diff suppressed because it is too large
Load Diff
@@ -1 +1 @@
|
|||||||
__version__ = "2.40.0"
|
__version__ = "2.41.0"
|
||||||
|
|||||||
@@ -104,7 +104,10 @@ doc_events = {
|
|||||||
"lms.lms.doctype.lms_badge.lms_badge.process_badges",
|
"lms.lms.doctype.lms_badge.lms_badge.process_badges",
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"Discussion Reply": {"after_insert": "lms.lms.utils.handle_notifications"},
|
"Discussion Reply": {
|
||||||
|
"after_insert": "lms.lms.utils.handle_notifications",
|
||||||
|
"validate": "lms.lms.utils.validate_discussion_reply",
|
||||||
|
},
|
||||||
"Notification Log": {"on_change": "lms.lms.utils.publish_notifications"},
|
"Notification Log": {"on_change": "lms.lms.utils.publish_notifications"},
|
||||||
"User": {
|
"User": {
|
||||||
"validate": "lms.lms.user.validate_username_duplicates",
|
"validate": "lms.lms.user.validate_username_duplicates",
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
"type",
|
"type",
|
||||||
"work_mode",
|
"work_mode",
|
||||||
"status",
|
"status",
|
||||||
"disabled",
|
|
||||||
"section_break_6",
|
"section_break_6",
|
||||||
"company_name",
|
"company_name",
|
||||||
"company_website",
|
"company_website",
|
||||||
@@ -97,12 +96,6 @@
|
|||||||
"label": "Company Logo",
|
"label": "Company Logo",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"fieldname": "disabled",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Disabled"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "company_email_address",
|
"fieldname": "company_email_address",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
@@ -137,8 +130,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2025-09-24 15:32:49.030004",
|
"modified": "2025-12-02 16:58:49.903274",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "Job",
|
"module": "Job",
|
||||||
"name": "Job Opportunity",
|
"name": "Job Opportunity",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
|
|||||||
@@ -520,7 +520,7 @@ def get_sidebar_settings():
|
|||||||
web_pages = frappe.get_all(
|
web_pages = frappe.get_all(
|
||||||
"LMS Sidebar Item",
|
"LMS Sidebar Item",
|
||||||
{"parenttype": "LMS Settings", "parentfield": "sidebar_items"},
|
{"parenttype": "LMS Settings", "parentfield": "sidebar_items"},
|
||||||
["web_page", "route", "title as label", "icon"],
|
["web_page", "route", "title as label", "icon", "name"],
|
||||||
)
|
)
|
||||||
for page in web_pages:
|
for page in web_pages:
|
||||||
page.to = page.route
|
page.to = page.route
|
||||||
@@ -1014,6 +1014,7 @@ def give_discussions_permission():
|
|||||||
"write": 1,
|
"write": 1,
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
|
"if_owner": 0 if role == "Moderator" else 1,
|
||||||
}
|
}
|
||||||
).save(ignore_permissions=True)
|
).save(ignore_permissions=True)
|
||||||
|
|
||||||
@@ -1303,7 +1304,24 @@ def get_notifications(filters):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_lms_setting(field):
|
def get_lms_setting(field=None):
|
||||||
|
if not field:
|
||||||
|
frappe.throw(_("Field name is required"))
|
||||||
|
frappe.log_error("Field name is missing when accessing LMS Settings {0} {1} {2}").format(
|
||||||
|
frappe.local.request_ip, frappe.get_request_header("Referer"), frappe.get_request_header("Origin")
|
||||||
|
)
|
||||||
|
|
||||||
|
allowed_fields = [
|
||||||
|
"allow_guest_access",
|
||||||
|
"prevent_skipping_videos",
|
||||||
|
"contact_us_email",
|
||||||
|
"contact_us_url",
|
||||||
|
"livecode_url",
|
||||||
|
]
|
||||||
|
|
||||||
|
if field not in allowed_fields:
|
||||||
|
frappe.throw(_("You are not allowed to access this field"))
|
||||||
|
|
||||||
return frappe.get_cached_value("LMS Settings", None, field)
|
return frappe.get_cached_value("LMS Settings", None, field)
|
||||||
|
|
||||||
|
|
||||||
@@ -1451,11 +1469,11 @@ def get_meta_info(type, route):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def update_meta_info(type, route, meta_tags):
|
def update_meta_info(meta_type, route, meta_tags):
|
||||||
parent_name = f"{type}/{route}"
|
validate_meta_data_permissions(meta_type)
|
||||||
if not isinstance(meta_tags, list):
|
validate_meta_tags(meta_tags)
|
||||||
frappe.throw(_("Meta tags should be a list."))
|
|
||||||
|
|
||||||
|
parent_name = f"{meta_type}/{route}"
|
||||||
for tag in meta_tags:
|
for tag in meta_tags:
|
||||||
existing_tag = frappe.db.exists(
|
existing_tag = frappe.db.exists(
|
||||||
"Website Meta Tag",
|
"Website Meta Tag",
|
||||||
@@ -1482,18 +1500,43 @@ def update_meta_info(type, route, meta_tags):
|
|||||||
|
|
||||||
parent_exists = frappe.db.exists("Website Route Meta", parent_name)
|
parent_exists = frappe.db.exists("Website Route Meta", parent_name)
|
||||||
if not parent_exists:
|
if not parent_exists:
|
||||||
route_meta = frappe.new_doc("Website Route Meta")
|
create_meta(parent_name, tag_properties)
|
||||||
route_meta.update(
|
|
||||||
{
|
|
||||||
"__newname": parent_name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
route_meta.append("meta_tags", tag_properties)
|
|
||||||
route_meta.insert()
|
|
||||||
else:
|
else:
|
||||||
new_tag = frappe.new_doc("Website Meta Tag")
|
create_meta_tag(tag_properties)
|
||||||
new_tag.update(tag_properties)
|
|
||||||
new_tag.insert()
|
|
||||||
|
def validate_meta_tags(meta_tags):
|
||||||
|
if not isinstance(meta_tags, list):
|
||||||
|
frappe.throw(_("Meta tags should be a list."))
|
||||||
|
|
||||||
|
|
||||||
|
def create_meta(parent_name, tag_properties):
|
||||||
|
route_meta = frappe.new_doc("Website Route Meta")
|
||||||
|
route_meta.update(
|
||||||
|
{
|
||||||
|
"__newname": parent_name,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
route_meta.append("meta_tags", tag_properties)
|
||||||
|
route_meta.insert()
|
||||||
|
|
||||||
|
|
||||||
|
def create_meta_tag(tag_properties):
|
||||||
|
new_tag = frappe.new_doc("Website Meta Tag")
|
||||||
|
new_tag.update(tag_properties)
|
||||||
|
new_tag.insert()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_meta_data_permissions(meta_type):
|
||||||
|
roles = frappe.get_roles()
|
||||||
|
|
||||||
|
if meta_type == "courses":
|
||||||
|
if not ("Course Creator" in roles or "Moderator" in roles):
|
||||||
|
frappe.throw(_("You do not have permission to update meta tags."))
|
||||||
|
|
||||||
|
elif meta_type == "batches":
|
||||||
|
if not ("Batch Evaluator" in roles or "Moderator" in roles):
|
||||||
|
frappe.throw(_("You do not have permission to update meta tags."))
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@@ -1678,7 +1721,18 @@ def get_profile_details(username):
|
|||||||
details = frappe.db.get_value(
|
details = frappe.db.get_value(
|
||||||
"User",
|
"User",
|
||||||
{"username": username},
|
{"username": username},
|
||||||
["full_name", "name", "username", "user_image", "bio", "headline", "cover_image"],
|
[
|
||||||
|
"first_name",
|
||||||
|
"last_name",
|
||||||
|
"full_name",
|
||||||
|
"name",
|
||||||
|
"username",
|
||||||
|
"user_image",
|
||||||
|
"bio",
|
||||||
|
"headline",
|
||||||
|
"language",
|
||||||
|
"cover_image",
|
||||||
|
],
|
||||||
as_dict=True,
|
as_dict=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -7,12 +7,12 @@
|
|||||||
"doctype": "Dashboard Chart",
|
"doctype": "Dashboard Chart",
|
||||||
"document_type": "LMS Certificate",
|
"document_type": "LMS Certificate",
|
||||||
"dynamic_filters_json": "[]",
|
"dynamic_filters_json": "[]",
|
||||||
"filters_json": "[[\"LMS Certificate\",\"published\",\"=\",1,false]]",
|
"filters_json": "[[\"LMS Certificate\",\"published\",\"=\",1]]",
|
||||||
"group_by_type": "Count",
|
"group_by_type": "Count",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_public": 1,
|
"is_public": 1,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"modified": "2025-04-28 17:47:28.517149",
|
"modified": "2025-12-07 17:47:28.517150",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "Certification",
|
"name": "Certification",
|
||||||
|
|||||||
@@ -9,19 +9,20 @@
|
|||||||
"doctype": "Dashboard Chart",
|
"doctype": "Dashboard Chart",
|
||||||
"document_type": "User",
|
"document_type": "User",
|
||||||
"dynamic_filters_json": "[]",
|
"dynamic_filters_json": "[]",
|
||||||
"filters_json": "[[\"User\",\"enabled\",\"=\",1,false]]",
|
"filters_json": "[[\"User\",\"enabled\",\"=\",1]]",
|
||||||
"group_by_type": "Count",
|
"group_by_type": "Count",
|
||||||
"idx": 5,
|
"idx": 5,
|
||||||
"is_public": 1,
|
"is_public": 1,
|
||||||
"is_standard": 1,
|
"is_standard": 1,
|
||||||
"last_synced_on": "2025-04-28 15:09:52.161688",
|
"last_synced_on": "2025-12-08 13:05:16.186243",
|
||||||
"modified": "2025-04-28 17:47:58.168293",
|
"modified": "2025-12-09 13:08:50.049053",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "New Signups",
|
"name": "New Signups",
|
||||||
"number_of_groups": 0,
|
"number_of_groups": 0,
|
||||||
"owner": "basawaraj@erpnext.com",
|
"owner": "basawaraj@erpnext.com",
|
||||||
"roles": [],
|
"roles": [],
|
||||||
|
"show_values_over_chart": 0,
|
||||||
"source": "",
|
"source": "",
|
||||||
"time_interval": "Daily",
|
"time_interval": "Daily",
|
||||||
"timeseries": 1,
|
"timeseries": 1,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
|
from lms.lms.utils import has_course_instructor_role, has_moderator_role
|
||||||
|
|
||||||
|
|
||||||
class LMSAssignment(Document):
|
class LMSAssignment(Document):
|
||||||
@@ -13,7 +13,7 @@ class LMSAssignment(Document):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def save_assignment(assignment, title, type, question):
|
def save_assignment(assignment, title, type, question):
|
||||||
if not has_course_moderator_role() or not has_course_instructor_role():
|
if not has_moderator_role() or not has_course_instructor_role():
|
||||||
return
|
return
|
||||||
|
|
||||||
if assignment:
|
if assignment:
|
||||||
|
|||||||
@@ -30,9 +30,7 @@ frappe.ui.form.on("LMS Badge", {
|
|||||||
|
|
||||||
const user_fields = fields
|
const user_fields = fields
|
||||||
.filter(
|
.filter(
|
||||||
(df) =>
|
(df) => df.fieldtype === "Link" && df.options === "User"
|
||||||
(df.fieldtype === "Link" && df.options === "User") ||
|
|
||||||
df.fieldtype === "Data"
|
|
||||||
)
|
)
|
||||||
.map(map_for_options)
|
.map(map_for_options)
|
||||||
.concat([
|
.concat([
|
||||||
|
|||||||
@@ -84,7 +84,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-11-10 11:39:42.233779",
|
"modified": "2025-12-04 17:06:26.090276",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Badge Assignment",
|
"name": "LMS Badge Assignment",
|
||||||
@@ -116,25 +116,13 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"if_owner": 1,
|
"if_owner": 1,
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"role": "LMS Student"
|
||||||
"role": "LMS Student",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"role": "LMS Student"
|
||||||
"role": "LMS Student",
|
|
||||||
"share": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create": 1,
|
"create": 1,
|
||||||
|
|||||||
@@ -1,9 +1,64 @@
|
|||||||
# Copyright (c) 2024, Frappe and contributors
|
# Copyright (c) 2024, Frappe and contributors
|
||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
# import frappe
|
import frappe
|
||||||
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
from lms.lms.doctype.lms_badge.lms_badge import eval_condition
|
||||||
|
|
||||||
|
|
||||||
class LMSBadgeAssignment(Document):
|
class LMSBadgeAssignment(Document):
|
||||||
pass
|
def validate(self):
|
||||||
|
self.validate_owner()
|
||||||
|
self.validate_duplicate_badge_assignment()
|
||||||
|
self.validate_badge_criteria()
|
||||||
|
|
||||||
|
def validate_owner(self):
|
||||||
|
if self.owner == self.member:
|
||||||
|
return
|
||||||
|
|
||||||
|
roles = frappe.get_roles(self.owner)
|
||||||
|
if "Moderator" not in roles:
|
||||||
|
frappe.throw(_("You must be a Moderator to assign badges to users."))
|
||||||
|
|
||||||
|
def validate_duplicate_badge_assignment(self):
|
||||||
|
grant_only_once = frappe.db.get_value("LMS Badge", self.badge, "grant_only_once")
|
||||||
|
if not grant_only_once:
|
||||||
|
return
|
||||||
|
|
||||||
|
if frappe.db.exists(
|
||||||
|
"LMS Badge Assignment",
|
||||||
|
{"badge": self.badge, "member": self.member, "name": ["!=", self.name]},
|
||||||
|
):
|
||||||
|
frappe.throw(
|
||||||
|
_("Badge {0} has already been assigned to this {1}.").format(self.badge, self.member)
|
||||||
|
)
|
||||||
|
|
||||||
|
def validate_badge_criteria(self):
|
||||||
|
badge_details = frappe.db.get_value(
|
||||||
|
"LMS Badge", self.badge, ["reference_doctype", "user_field", "condition", "enabled"], as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if badge_details:
|
||||||
|
if badge_details.reference_doctype and badge_details.user_field and badge_details.condition:
|
||||||
|
user_fieldname = frappe.db.get_value(
|
||||||
|
"DocField",
|
||||||
|
{"parent": badge_details.reference_doctype, "fieldname": badge_details.user_field},
|
||||||
|
"fieldname",
|
||||||
|
)
|
||||||
|
|
||||||
|
documents = frappe.get_all(
|
||||||
|
badge_details.reference_doctype,
|
||||||
|
{user_fieldname: self.member},
|
||||||
|
)
|
||||||
|
|
||||||
|
for document in documents:
|
||||||
|
reference_value = eval_condition(
|
||||||
|
frappe.get_doc(badge_details.reference_doctype, document.name),
|
||||||
|
badge_details.condition,
|
||||||
|
)
|
||||||
|
if reference_value:
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.throw(_("Member does not meet the criteria for the badge {0}.").format(self.badge))
|
||||||
|
|||||||
@@ -379,7 +379,7 @@
|
|||||||
"link_fieldname": "batch_name"
|
"link_fieldname": "batch_name"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2025-05-26 15:30:55.083507",
|
"modified": "2025-12-04 12:54:11.190967",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch",
|
"name": "LMS Batch",
|
||||||
@@ -422,13 +422,8 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"role": "LMS Student"
|
||||||
"role": "LMS Student",
|
|
||||||
"share": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"row_format": "Dynamic",
|
"row_format": "Dynamic",
|
||||||
|
|||||||
@@ -73,8 +73,8 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-02-11 10:39:57.259526",
|
"modified": "2025-12-04 12:53:38.246250",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Batch Enrollment",
|
"name": "LMS Batch Enrollment",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
@@ -105,18 +105,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"if_owner": 1,
|
"if_owner": 1,
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"role": "LMS Student"
|
||||||
"role": "LMS Student",
|
|
||||||
"share": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "creation",
|
"sort_field": "creation",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "member_name"
|
"title_field": "member_name"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,19 @@ class LMSBatchEnrollment(Document):
|
|||||||
self.add_member_to_live_class()
|
self.add_member_to_live_class()
|
||||||
|
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
self.validate_owner()
|
||||||
self.validate_duplicate_members()
|
self.validate_duplicate_members()
|
||||||
|
self.validate_seat_availability()
|
||||||
self.validate_course_enrollment()
|
self.validate_course_enrollment()
|
||||||
|
|
||||||
|
def validate_owner(self):
|
||||||
|
if self.owner == self.member:
|
||||||
|
return
|
||||||
|
|
||||||
|
roles = frappe.get_roles(self.owner)
|
||||||
|
if not ("Moderator" in roles or "Batch Evaluator" in roles):
|
||||||
|
frappe.throw(_("You must be a Moderator or Batch Evaluator to enroll users in a batch."))
|
||||||
|
|
||||||
def validate_duplicate_members(self):
|
def validate_duplicate_members(self):
|
||||||
if frappe.db.exists(
|
if frappe.db.exists(
|
||||||
"LMS Batch Enrollment",
|
"LMS Batch Enrollment",
|
||||||
@@ -25,6 +35,12 @@ class LMSBatchEnrollment(Document):
|
|||||||
):
|
):
|
||||||
frappe.throw(_("Member already enrolled in this batch"))
|
frappe.throw(_("Member already enrolled in this batch"))
|
||||||
|
|
||||||
|
def validate_seat_availability(self):
|
||||||
|
seat_count = frappe.db.get_value("LMS Batch", self.batch, "seat_count")
|
||||||
|
enrolled_count = frappe.db.count("LMS Batch Enrollment", {"batch": self.batch})
|
||||||
|
if seat_count and enrolled_count >= seat_count:
|
||||||
|
frappe.throw(_("There are no seats available in this batch."))
|
||||||
|
|
||||||
def validate_course_enrollment(self):
|
def validate_course_enrollment(self):
|
||||||
courses = frappe.get_all("Batch Course", filters={"parent": self.batch}, fields=["course"])
|
courses = frappe.get_all("Batch Course", filters={"parent": self.batch}, fields=["course"])
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
{
|
{
|
||||||
"actions": [],
|
"actions": [],
|
||||||
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "field:category",
|
"autoname": "field:category",
|
||||||
"creation": "2023-06-15 12:40:36.484165",
|
"creation": "2023-06-15 12:40:36.484165",
|
||||||
@@ -21,8 +22,8 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-03-19 12:12:23.723432",
|
"modified": "2025-11-08 19:28:28.468137",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Category",
|
"name": "LMS Category",
|
||||||
"naming_rule": "By fieldname",
|
"naming_rule": "By fieldname",
|
||||||
@@ -73,9 +74,10 @@
|
|||||||
"share": 1
|
"share": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"row_format": "Dynamic",
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"title_field": "category",
|
"title_field": "category",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ class LMSCertificate(Document):
|
|||||||
self.name = make_autoname("hash", self.doctype)
|
self.name = make_autoname("hash", self.doctype)
|
||||||
|
|
||||||
def after_insert(self):
|
def after_insert(self):
|
||||||
if not frappe.flags.in_test:
|
if not frappe.in_test:
|
||||||
outgoing_email_account = frappe.get_cached_value(
|
outgoing_email_account = frappe.get_cached_value(
|
||||||
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
"Email Account", {"default_outgoing": 1, "enable_outgoing": 1}, "name"
|
||||||
)
|
)
|
||||||
@@ -115,29 +115,14 @@ def has_website_permission(doc, ptype, user, verbose=False):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_certificate(course):
|
def create_certificate(course):
|
||||||
certificate = is_certified(course)
|
if is_certified(course):
|
||||||
|
|
||||||
if certificate:
|
|
||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"LMS Certificate", certificate, ["name", "course", "template"], as_dict=True
|
"LMS Certificate", certificate, ["name", "course", "template"], as_dict=True
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
default_certificate_template = frappe.db.get_value(
|
validate_certification_eligibility(course)
|
||||||
"Property Setter",
|
default_certificate_template = get_default_certificate_template()
|
||||||
{
|
|
||||||
"doc_type": "LMS Certificate",
|
|
||||||
"property": "default_print_format",
|
|
||||||
},
|
|
||||||
"value",
|
|
||||||
)
|
|
||||||
if not default_certificate_template:
|
|
||||||
default_certificate_template = frappe.db.get_value(
|
|
||||||
"Print Format",
|
|
||||||
{
|
|
||||||
"doc_type": "LMS Certificate",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
certificate = frappe.get_doc(
|
certificate = frappe.get_doc(
|
||||||
{
|
{
|
||||||
"doctype": "LMS Certificate",
|
"doctype": "LMS Certificate",
|
||||||
@@ -149,3 +134,37 @@ def create_certificate(course):
|
|||||||
)
|
)
|
||||||
certificate.save(ignore_permissions=True)
|
certificate.save(ignore_permissions=True)
|
||||||
return certificate
|
return certificate
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_certificate_template():
|
||||||
|
default_certificate_template = frappe.db.get_value(
|
||||||
|
"Property Setter",
|
||||||
|
{
|
||||||
|
"doc_type": "LMS Certificate",
|
||||||
|
"property": "default_print_format",
|
||||||
|
},
|
||||||
|
"value",
|
||||||
|
)
|
||||||
|
if not default_certificate_template:
|
||||||
|
default_certificate_template = frappe.db.get_value(
|
||||||
|
"Print Format",
|
||||||
|
{
|
||||||
|
"doc_type": "LMS Certificate",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
return default_certificate_template
|
||||||
|
|
||||||
|
|
||||||
|
def validate_certification_eligibility(course):
|
||||||
|
if not frappe.db.exists("LMS Enrollment", {"course": course, "member": frappe.session.user}):
|
||||||
|
frappe.throw(_("You are not enrolled in this course."))
|
||||||
|
|
||||||
|
if not frappe.db.get_value("LMS Course", course, "enable_certification"):
|
||||||
|
frappe.throw(_("Certification is not enabled for this course."))
|
||||||
|
|
||||||
|
progress = frappe.db.get_value(
|
||||||
|
"LMS Enrollment", {"course": course, "member": frappe.session.user}, "progress"
|
||||||
|
)
|
||||||
|
if progress < 100:
|
||||||
|
frappe.throw(_("You have not completed the course yet."))
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_years, cint, nowdate
|
from frappe.utils import cint, nowdate
|
||||||
|
|
||||||
from lms.lms.doctype.lms_certificate.lms_certificate import create_certificate
|
from lms.lms.doctype.lms_certificate.lms_certificate import create_certificate
|
||||||
from lms.lms.doctype.lms_course.test_lms_course import new_course
|
from lms.lms.doctype.lms_course.test_lms_course import new_course
|
||||||
@@ -18,6 +18,7 @@ class TestLMSCertificate(unittest.TestCase):
|
|||||||
"enable_certification": 1,
|
"enable_certification": 1,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
create_enrollment(course.name)
|
||||||
certificate = create_certificate(course.name)
|
certificate = create_certificate(course.name)
|
||||||
|
|
||||||
self.assertEqual(certificate.member, "Administrator")
|
self.assertEqual(certificate.member, "Administrator")
|
||||||
@@ -26,3 +27,11 @@ class TestLMSCertificate(unittest.TestCase):
|
|||||||
|
|
||||||
frappe.db.delete("LMS Certificate", certificate.name)
|
frappe.db.delete("LMS Certificate", certificate.name)
|
||||||
frappe.db.delete("LMS Course", course.name)
|
frappe.db.delete("LMS Course", course.name)
|
||||||
|
|
||||||
|
|
||||||
|
def create_enrollment(course):
|
||||||
|
enrollment = frappe.new_doc("LMS Enrollment")
|
||||||
|
enrollment.course = course
|
||||||
|
enrollment.member = frappe.session.user
|
||||||
|
enrollment.progress = cint(100)
|
||||||
|
enrollment.save()
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
|
||||||
from lms.lms.utils import has_course_moderator_role
|
from lms.lms.utils import has_moderator_role
|
||||||
|
|
||||||
|
|
||||||
class LMSCertificateEvaluation(Document):
|
class LMSCertificateEvaluation(Document):
|
||||||
@@ -19,7 +19,7 @@ class LMSCertificateEvaluation(Document):
|
|||||||
|
|
||||||
|
|
||||||
def has_website_permission(doc, ptype, user, verbose=False):
|
def has_website_permission(doc, ptype, user, verbose=False):
|
||||||
if has_course_moderator_role() or doc.member == frappe.session.user:
|
if has_moderator_role() or doc.member == frappe.session.user:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class TestLMSCourse(unittest.TestCase):
|
|||||||
frappe.delete_doc("User", "tester@example.com")
|
frappe.delete_doc("User", "tester@example.com")
|
||||||
|
|
||||||
if frappe.db.exists("LMS Course", "test-course"):
|
if frappe.db.exists("LMS Course", "test-course"):
|
||||||
|
frappe.db.delete("Batch Course", {"course": "test-course"})
|
||||||
frappe.db.delete("Exercise Submission", {"course": "test-course"})
|
frappe.db.delete("Exercise Submission", {"course": "test-course"})
|
||||||
frappe.db.delete("Exercise Latest Submission", {"course": "test-course"})
|
frappe.db.delete("Exercise Latest Submission", {"course": "test-course"})
|
||||||
frappe.db.delete("LMS Exercise", {"course": "test-course"})
|
frappe.db.delete("LMS Exercise", {"course": "test-course"})
|
||||||
|
|||||||
@@ -77,8 +77,7 @@ def update_program_progress(member):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_membership(course, batch=None, member=None, member_type="Student", role="Member"):
|
def create_membership(course, batch=None, member=None, member_type="Student", role="Member"):
|
||||||
if frappe.db.get_value("LMS Course", course, "disable_self_learning"):
|
validate_course_enrollment_eligibility(course, member)
|
||||||
return False
|
|
||||||
|
|
||||||
enrollment = frappe.new_doc("LMS Enrollment")
|
enrollment = frappe.new_doc("LMS Enrollment")
|
||||||
enrollment.update(
|
enrollment.update(
|
||||||
@@ -95,6 +94,42 @@ def create_membership(course, batch=None, member=None, member_type="Student", ro
|
|||||||
return enrollment
|
return enrollment
|
||||||
|
|
||||||
|
|
||||||
|
def validate_course_enrollment_eligibility(course, member):
|
||||||
|
if not member:
|
||||||
|
member = frappe.session.user
|
||||||
|
|
||||||
|
course_details = frappe.db.get_value(
|
||||||
|
"LMS Course",
|
||||||
|
course,
|
||||||
|
["published", "disable_self_learning", "paid_course", "paid_certificate"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
if course_details.disable_self_learning:
|
||||||
|
frappe.throw(
|
||||||
|
_(
|
||||||
|
"You cannot enroll in this course as self-learning is disabled. Please contact the Administrator."
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not course_details.published:
|
||||||
|
frappe.throw(_("You cannot enroll in an unpublished course."))
|
||||||
|
|
||||||
|
if course_details.paid_course:
|
||||||
|
payment = frappe.db.exists(
|
||||||
|
"LMS Payment",
|
||||||
|
{
|
||||||
|
"reference_doctype": "LMS Course",
|
||||||
|
"reference_docname": course,
|
||||||
|
"member": member,
|
||||||
|
"payment_receipt": True,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if not payment:
|
||||||
|
frappe.throw(_("You need to complete the payment for this course before enrolling."))
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def update_current_membership(batch, course, member):
|
def update_current_membership(batch, course, member):
|
||||||
all_memberships = frappe.get_all("LMS Enrollment", {"member": member, "course": course})
|
all_memberships = frappe.get_all("LMS Enrollment", {"member": member, "course": course})
|
||||||
|
|||||||
@@ -9,60 +9,15 @@ from lms.lms.doctype.lms_course.test_lms_course import new_course, new_user
|
|||||||
|
|
||||||
|
|
||||||
class TestLMSEnrollment(unittest.TestCase):
|
class TestLMSEnrollment(unittest.TestCase):
|
||||||
def setUp(self):
|
|
||||||
frappe.db.delete("LMS Enrollment")
|
|
||||||
frappe.db.delete("LMS Batch Old")
|
|
||||||
frappe.db.delete("LMS Course Mentor Mapping")
|
|
||||||
frappe.db.delete("User", {"email": ("like", "%@test.com")})
|
|
||||||
|
|
||||||
def new_course_batch(self):
|
|
||||||
course = new_course("Test Course")
|
|
||||||
|
|
||||||
new_user("Test Mentor", "mentor@test.com")
|
|
||||||
# without this, the creating batch will fail
|
|
||||||
course.add_mentor("mentor@test.com")
|
|
||||||
|
|
||||||
frappe.session.user = "mentor@test.com"
|
|
||||||
|
|
||||||
batch = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "LMS Batch Old",
|
|
||||||
"name": "test-batch",
|
|
||||||
"title": "Test Batch",
|
|
||||||
"course": course.name,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
batch.insert(ignore_permissions=True)
|
|
||||||
|
|
||||||
frappe.session.user = "Administrator"
|
|
||||||
return course, batch
|
|
||||||
|
|
||||||
def add_membership(self, batch_name, member_name, course, member_type="Student"):
|
|
||||||
doc = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "LMS Enrollment",
|
|
||||||
"batch_old": batch_name,
|
|
||||||
"member": member_name,
|
|
||||||
"member_type": member_type,
|
|
||||||
"course": course,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
doc.insert()
|
|
||||||
return doc
|
|
||||||
|
|
||||||
def test_membership(self):
|
def test_membership(self):
|
||||||
course, batch = self.new_course_batch()
|
course = new_course("Test Enrollment")
|
||||||
member = new_user("Test", "test01@test.com")
|
enrollment = frappe.new_doc("LMS Enrollment")
|
||||||
membership = self.add_membership(batch.name, member.name, course.name)
|
enrollment.course = course.name
|
||||||
|
enrollment.member = frappe.session.user
|
||||||
|
|
||||||
assert membership.course == course.name
|
enrollment.save()
|
||||||
assert membership.member_name == member.full_name
|
|
||||||
|
|
||||||
def test_membership_change_role(self):
|
self.assertEqual(enrollment.course, course.name)
|
||||||
course, batch = self.new_course_batch()
|
self.assertEqual(enrollment.member, "Administrator")
|
||||||
member = new_user("Test", "test01@test.com")
|
frappe.db.delete("LMS Enrollment", enrollment.name)
|
||||||
membership = self.add_membership(batch.name, member.name, course.name)
|
frappe.db.delete("LMS Course", course.name)
|
||||||
|
|
||||||
# it should be possible to change role
|
|
||||||
membership.role = "Admin"
|
|
||||||
membership.save()
|
|
||||||
|
|||||||
@@ -3,52 +3,8 @@
|
|||||||
|
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
# import frappe
|
||||||
|
|
||||||
from lms.lms.doctype.lms_course.test_lms_course import new_course
|
|
||||||
|
|
||||||
|
|
||||||
class TestLMSExercise(unittest.TestCase):
|
class TestLMSExercise(unittest.TestCase):
|
||||||
def new_exercise(self):
|
pass
|
||||||
course = new_course("Test Course")
|
|
||||||
member = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "LMS Enrollment",
|
|
||||||
"course": course.name,
|
|
||||||
"member": frappe.session.user,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
member.insert()
|
|
||||||
e = frappe.get_doc(
|
|
||||||
{
|
|
||||||
"doctype": "LMS Exercise",
|
|
||||||
"name": "test-problem",
|
|
||||||
"course": course.name,
|
|
||||||
"title": "Test Problem",
|
|
||||||
"description": "draw a circle",
|
|
||||||
"code": "# draw a single cicle",
|
|
||||||
"answer": ("# draw a single circle\n" + "circle(100, 100, 50)"),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
e.insert()
|
|
||||||
return e
|
|
||||||
|
|
||||||
def test_exercise(self):
|
|
||||||
e = self.new_exercise()
|
|
||||||
assert e.get_user_submission() is None
|
|
||||||
|
|
||||||
def test_exercise_submission(self):
|
|
||||||
e = self.new_exercise()
|
|
||||||
submission = e.submit("circle(100, 100, 50)")
|
|
||||||
assert submission is not None
|
|
||||||
assert submission.exercise == e.name
|
|
||||||
assert submission.course == e.course
|
|
||||||
|
|
||||||
user_submission = e.get_user_submission()
|
|
||||||
assert user_submission is not None
|
|
||||||
assert user_submission.name == submission.name
|
|
||||||
|
|
||||||
def tearDown(self):
|
|
||||||
frappe.db.delete("LMS Enrollment")
|
|
||||||
frappe.db.delete("Exercise Submission")
|
|
||||||
frappe.db.delete("LMS Exercise")
|
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
"grid_page_length": 50,
|
"grid_page_length": 50,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-08-20 12:28:57.238902",
|
"modified": "2025-12-04 12:56:14.249363",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Program",
|
"name": "LMS Program",
|
||||||
@@ -136,13 +136,8 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"role": "LMS Student"
|
||||||
"role": "LMS Student",
|
|
||||||
"share": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"row_format": "Dynamic",
|
"row_format": "Dynamic",
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ class LMSProgram(Document):
|
|||||||
duplicates = {course for course in courses if courses.count(course) > 1}
|
duplicates = {course for course in courses if courses.count(course) > 1}
|
||||||
if len(duplicates):
|
if len(duplicates):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Course {0} has already been added to this batch.").format(
|
_("Course {0} has already been added to this program.").format(
|
||||||
frappe.bold(next(iter(duplicates)))
|
frappe.bold(next(iter(duplicates)))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -27,7 +27,7 @@ class LMSProgram(Document):
|
|||||||
duplicates = {member for member in members if members.count(member) > 1}
|
duplicates = {member for member in members if members.count(member) > 1}
|
||||||
if len(duplicates):
|
if len(duplicates):
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Member {0} has already been added to this batch.").format(
|
_("Member {0} has already been added to this program.").format(
|
||||||
frappe.bold(next(iter(duplicates)))
|
frappe.bold(next(iter(duplicates)))
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
from lms.lms.utils import has_course_instructor_role, has_course_moderator_role
|
from lms.lms.utils import has_course_instructor_role, has_moderator_role
|
||||||
|
|
||||||
|
|
||||||
class LMSQuestion(Document):
|
class LMSQuestion(Document):
|
||||||
@@ -95,7 +95,7 @@ def get_correct_options(question):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_question_details(question):
|
def get_question_details(question):
|
||||||
if not has_course_instructor_role() or not has_course_moderator_role():
|
if not has_course_instructor_role() or not has_moderator_role():
|
||||||
return
|
return
|
||||||
|
|
||||||
fields = ["question", "type", "name"]
|
fields = ["question", "type", "name"]
|
||||||
|
|||||||
@@ -352,7 +352,7 @@
|
|||||||
"options": "Email Template"
|
"options": "Email Template"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "1",
|
||||||
"fieldname": "disable_signup",
|
"fieldname": "disable_signup",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Disable Signup"
|
"label": "Disable Signup"
|
||||||
@@ -444,7 +444,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2025-10-07 19:22:48.705933",
|
"modified": "2025-12-02 12:21:15.832799",
|
||||||
"modified_by": "sayali@frappe.io",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS Settings",
|
"name": "LMS Settings",
|
||||||
|
|||||||
196
lms/lms/utils.py
196
lms/lms/utils.py
@@ -13,7 +13,6 @@ from frappe.desk.notifications import extract_mentions
|
|||||||
from frappe.rate_limiter import rate_limit
|
from frappe.rate_limiter import rate_limit
|
||||||
from frappe.utils import (
|
from frappe.utils import (
|
||||||
add_months,
|
add_months,
|
||||||
ceil,
|
|
||||||
cint,
|
cint,
|
||||||
cstr,
|
cstr,
|
||||||
flt,
|
flt,
|
||||||
@@ -25,6 +24,7 @@ from frappe.utils import (
|
|||||||
getdate,
|
getdate,
|
||||||
nowtime,
|
nowtime,
|
||||||
pretty_date,
|
pretty_date,
|
||||||
|
rounded,
|
||||||
)
|
)
|
||||||
|
|
||||||
from lms.lms.md import find_macros, markdown_to_html
|
from lms.lms.md import find_macros, markdown_to_html
|
||||||
@@ -201,7 +201,7 @@ def get_lesson_icon(body, content):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@rate_limit(limit=50, seconds=60 * 60)
|
@rate_limit(limit=500, seconds=60 * 60)
|
||||||
def get_tags(course):
|
def get_tags(course):
|
||||||
tags = frappe.db.get_value("LMS Course", course, "tags")
|
tags = frappe.db.get_value("LMS Course", course, "tags")
|
||||||
return tags.split(",") if tags else []
|
return tags.split(",") if tags else []
|
||||||
@@ -246,7 +246,7 @@ def get_average_rating(course):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@rate_limit(limit=50, seconds=60 * 60)
|
@rate_limit(limit=500, seconds=60 * 60)
|
||||||
def get_reviews(course):
|
def get_reviews(course):
|
||||||
reviews = frappe.get_all(
|
reviews = frappe.get_all(
|
||||||
"LMS Course Review",
|
"LMS Course Review",
|
||||||
@@ -492,7 +492,7 @@ def can_create_courses(course, member=None):
|
|||||||
if frappe.session.user == "Guest":
|
if frappe.session.user == "Guest":
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if has_course_moderator_role(member):
|
if has_moderator_role(member):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
if has_course_instructor_role(member) and member in instructors:
|
if has_course_instructor_role(member) and member in instructors:
|
||||||
@@ -504,7 +504,18 @@ def can_create_courses(course, member=None):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def has_course_moderator_role(member=None):
|
def can_create_batches(member=None):
|
||||||
|
if not member:
|
||||||
|
member = frappe.session.user
|
||||||
|
|
||||||
|
if has_moderator_role(member):
|
||||||
|
return True
|
||||||
|
if has_evaluator_role(member):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def has_moderator_role(member=None):
|
||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"Has Role",
|
"Has Role",
|
||||||
{"parent": member or frappe.session.user, "role": "Moderator"},
|
{"parent": member or frappe.session.user, "role": "Moderator"},
|
||||||
@@ -512,7 +523,7 @@ def has_course_moderator_role(member=None):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def has_course_evaluator_role(member=None):
|
def has_evaluator_role(member=None):
|
||||||
return frappe.db.get_value(
|
return frappe.db.get_value(
|
||||||
"Has Role",
|
"Has Role",
|
||||||
{"parent": member or frappe.session.user, "role": "Batch Evaluator"},
|
{"parent": member or frappe.session.user, "role": "Batch Evaluator"},
|
||||||
@@ -737,7 +748,7 @@ def has_lessons(course):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@rate_limit(limit=50, seconds=60 * 60)
|
@rate_limit(limit=500, seconds=60 * 60)
|
||||||
def get_chart_data(
|
def get_chart_data(
|
||||||
chart_name,
|
chart_name,
|
||||||
timespan="Select Date Range",
|
timespan="Select Date Range",
|
||||||
@@ -758,17 +769,18 @@ def get_chart_data(
|
|||||||
datefield = chart.based_on
|
datefield = chart.based_on
|
||||||
value_field = chart.value_based_on or "1"
|
value_field = chart.value_based_on or "1"
|
||||||
|
|
||||||
filters = [([chart.document_type, "docstatus", "<", 2, False])]
|
filters = [([chart.document_type, "docstatus", "<", 2])]
|
||||||
|
print(chart.filters_json)
|
||||||
filters = filters + json.loads(chart.filters_json)
|
filters = filters + json.loads(chart.filters_json)
|
||||||
filters.append([doctype, datefield, ">=", from_date, False])
|
filters.append([doctype, datefield, ">=", from_date])
|
||||||
filters.append([doctype, datefield, "<=", to_date, False])
|
filters.append([doctype, datefield, "<=", to_date])
|
||||||
|
|
||||||
data = frappe.db.get_all(
|
data = frappe.db.get_all(
|
||||||
doctype,
|
doctype,
|
||||||
fields=[f"{datefield} as _unit", f"SUM({value_field})", "COUNT(*)"],
|
fields=[datefield, {"SUM": value_field}, {"COUNT": "*"}],
|
||||||
filters=filters,
|
filters=filters,
|
||||||
group_by="_unit",
|
group_by=datefield,
|
||||||
order_by="_unit asc",
|
order_by=datefield,
|
||||||
as_list=True,
|
as_list=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -785,7 +797,7 @@ def get_chart_data(
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@rate_limit(limit=50, seconds=60 * 60)
|
@rate_limit(limit=500, seconds=60 * 60)
|
||||||
def get_course_completion_data():
|
def get_course_completion_data():
|
||||||
all_membership = frappe.db.count("LMS Enrollment")
|
all_membership = frappe.db.count("LMS Enrollment")
|
||||||
completed = frappe.db.count("LMS Enrollment", {"progress": ["like", "%100%"]})
|
completed = frappe.db.count("LMS Enrollment", {"progress": ["like", "%100%"]})
|
||||||
@@ -812,7 +824,7 @@ def get_telemetry_boot_info():
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def is_onboarding_complete():
|
def is_onboarding_complete():
|
||||||
if not has_course_moderator_role():
|
if not has_moderator_role():
|
||||||
return {"is_onboarded": True}
|
return {"is_onboarded": True}
|
||||||
|
|
||||||
course_created = frappe.db.a_row_exists("LMS Course")
|
course_created = frappe.db.a_row_exists("LMS Course")
|
||||||
@@ -928,7 +940,7 @@ def check_multicurrency(amount, currency, country=None, amount_usd=None):
|
|||||||
if apply_rounding and amount % 100 != 0:
|
if apply_rounding and amount % 100 != 0:
|
||||||
amount = amount + 100 - amount % 100
|
amount = amount + 100 - amount % 100
|
||||||
|
|
||||||
return ceil(amount), currency
|
return rounded(amount), currency
|
||||||
|
|
||||||
|
|
||||||
def apply_gst(amount, country=None):
|
def apply_gst(amount, country=None):
|
||||||
@@ -961,7 +973,7 @@ def change_currency(amount, currency, country=None):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@rate_limit(limit=50, seconds=60 * 60)
|
@rate_limit(limit=500, seconds=60 * 60)
|
||||||
def get_courses(filters=None, start=0):
|
def get_courses(filters=None, start=0):
|
||||||
"""Returns the list of courses."""
|
"""Returns the list of courses."""
|
||||||
|
|
||||||
@@ -1102,7 +1114,7 @@ def get_course_fields():
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@rate_limit(limit=50, seconds=60 * 60)
|
@rate_limit(limit=500, seconds=60 * 60)
|
||||||
def get_course_details(course):
|
def get_course_details(course):
|
||||||
course_details = frappe.db.get_value(
|
course_details = frappe.db.get_value(
|
||||||
"LMS Course",
|
"LMS Course",
|
||||||
@@ -1197,7 +1209,6 @@ def get_categorized_courses(courses):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@rate_limit(limit=50, seconds=60 * 60)
|
|
||||||
def get_course_outline(course, progress=False):
|
def get_course_outline(course, progress=False):
|
||||||
"""Returns the course outline."""
|
"""Returns the course outline."""
|
||||||
outline = []
|
outline = []
|
||||||
@@ -1225,7 +1236,7 @@ def get_course_outline(course, progress=False):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@rate_limit(limit=50, seconds=60 * 60)
|
@rate_limit(limit=500, seconds=60 * 60)
|
||||||
def get_lesson(course, chapter, lesson):
|
def get_lesson(course, chapter, lesson):
|
||||||
chapter_name = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": chapter}, "chapter")
|
chapter_name = frappe.db.get_value("Chapter Reference", {"parent": course, "idx": chapter}, "chapter")
|
||||||
lesson_name = frappe.db.get_value("Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson")
|
lesson_name = frappe.db.get_value("Lesson Reference", {"parent": chapter_name, "idx": lesson}, "lesson")
|
||||||
@@ -1256,7 +1267,7 @@ def get_lesson(course, chapter, lesson):
|
|||||||
if (
|
if (
|
||||||
not lesson_details.include_in_preview
|
not lesson_details.include_in_preview
|
||||||
and not membership
|
and not membership
|
||||||
and not has_course_moderator_role()
|
and not has_moderator_role()
|
||||||
and not is_instructor(course)
|
and not is_instructor(course)
|
||||||
):
|
):
|
||||||
return {
|
return {
|
||||||
@@ -1336,12 +1347,12 @@ def get_neighbour_lesson(course, chapter, lesson):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@rate_limit(limit=50, seconds=60 * 60)
|
@rate_limit(limit=500, seconds=60 * 60)
|
||||||
def get_batch_details(batch):
|
def get_batch_details(batch):
|
||||||
batch_students = frappe.get_all("LMS Batch Enrollment", {"batch": batch}, pluck="member")
|
batch_students = frappe.get_all("LMS Batch Enrollment", {"batch": batch}, pluck="member")
|
||||||
if (
|
if (
|
||||||
not frappe.db.get_value("LMS Batch", batch, "published")
|
not frappe.db.get_value("LMS Batch", batch, "published")
|
||||||
and has_student_role()
|
and not can_create_batches()
|
||||||
and frappe.session.user not in batch_students
|
and frappe.session.user not in batch_students
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
@@ -1457,7 +1468,7 @@ def get_question_details(question):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@rate_limit(limit=50, seconds=60 * 60)
|
@rate_limit(limit=500, seconds=60 * 60)
|
||||||
def get_batch_courses(batch):
|
def get_batch_courses(batch):
|
||||||
courses = []
|
courses = []
|
||||||
course_list = frappe.get_all("Batch Course", {"parent": batch}, ["name", "course"])
|
course_list = frappe.get_all("Batch Course", {"parent": batch}, ["name", "course"])
|
||||||
@@ -1681,6 +1692,11 @@ def has_submitted_assessment(assessment, assessment_type, member=None):
|
|||||||
docfield = "quiz"
|
docfield = "quiz"
|
||||||
fields = ["percentage"]
|
fields = ["percentage"]
|
||||||
not_attempted = 0
|
not_attempted = 0
|
||||||
|
elif assessment_type == "LMS Programming Exercise":
|
||||||
|
doctype = "LMS Programming Exercise Submission"
|
||||||
|
docfield = "exercise"
|
||||||
|
fields = ["status"]
|
||||||
|
not_attempted = "Not Attempted"
|
||||||
|
|
||||||
filters = {}
|
filters = {}
|
||||||
filters[docfield] = assessment
|
filters[docfield] = assessment
|
||||||
@@ -1944,9 +1960,9 @@ def get_lesson_creation_details(course, chapter, lesson):
|
|||||||
def get_roles(name):
|
def get_roles(name):
|
||||||
frappe.only_for("Moderator")
|
frappe.only_for("Moderator")
|
||||||
return {
|
return {
|
||||||
"moderator": has_course_moderator_role(name),
|
"moderator": has_moderator_role(name),
|
||||||
"course_creator": has_course_instructor_role(name),
|
"course_creator": has_course_instructor_role(name),
|
||||||
"batch_evaluator": has_course_evaluator_role(name),
|
"batch_evaluator": has_evaluator_role(name),
|
||||||
"lms_student": has_student_role(name),
|
"lms_student": has_student_role(name),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2048,29 +2064,59 @@ def enroll_in_course(course, payment_name):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def enroll_in_batch(batch, payment_name=None):
|
def enroll_in_batch(batch, payment_name=None):
|
||||||
if not frappe.db.exists("LMS Batch Enrollment", {"batch": batch, "member": frappe.session.user}):
|
if not frappe.db.exists("LMS Batch", batch):
|
||||||
batch_doc = frappe.db.get_value("LMS Batch", batch, ["name", "seat_count"], as_dict=True)
|
frappe.throw(_("The specified batch does not exist."))
|
||||||
students = frappe.db.count("LMS Batch Enrollment", {"batch": batch})
|
|
||||||
if batch_doc.seat_count and students >= batch_doc.seat_count:
|
|
||||||
frappe.throw(_("The batch is full. Please contact the Administrator."))
|
|
||||||
|
|
||||||
new_student = frappe.new_doc("LMS Batch Enrollment")
|
batch_doc = frappe.db.get_value(
|
||||||
|
"LMS Batch", batch, ["name", "seat_count", "allow_self_enrollment"], as_dict=True
|
||||||
|
)
|
||||||
|
payment_doc = get_payment_details(payment_name)
|
||||||
|
validate_enrollment_eligibility(batch_doc, payment_doc)
|
||||||
|
create_enrollment(batch, payment_doc)
|
||||||
|
|
||||||
|
|
||||||
|
def get_payment_details(payment_name):
|
||||||
|
payment_doc = None
|
||||||
|
if payment_name:
|
||||||
|
payment_doc = frappe.db.get_value(
|
||||||
|
"LMS Payment", payment_name, ["name", "source", "payment_received"], as_dict=True
|
||||||
|
)
|
||||||
|
return payment_doc
|
||||||
|
|
||||||
|
|
||||||
|
def validate_enrollment_eligibility(batch_doc, payment_doc=None):
|
||||||
|
if frappe.db.exists("LMS Batch Enrollment", {"batch": batch_doc.name, "member": frappe.session.user}):
|
||||||
|
frappe.throw(_("You are already enrolled in this batch."))
|
||||||
|
|
||||||
|
if batch_doc.paid_batch:
|
||||||
|
if not payment_doc or not payment_doc.payment_received:
|
||||||
|
frappe.throw(_("Payment is required to enroll in this batch."))
|
||||||
|
|
||||||
|
elif not batch_doc.allow_self_enrollment:
|
||||||
|
frappe.throw(_("Enrollment in this batch is restricted. Please contact the Administrator."))
|
||||||
|
|
||||||
|
students = frappe.db.count("LMS Batch Enrollment", {"batch": batch_doc.name})
|
||||||
|
if batch_doc.seat_count and students >= batch_doc.seat_count:
|
||||||
|
frappe.throw(_("There are no seats available in this batch."))
|
||||||
|
|
||||||
|
|
||||||
|
def create_enrollment(batch, payment_doc=None):
|
||||||
|
new_student = frappe.new_doc("LMS Batch Enrollment")
|
||||||
|
new_student.update(
|
||||||
|
{
|
||||||
|
"member": frappe.session.user,
|
||||||
|
"batch": batch,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if payment_doc:
|
||||||
new_student.update(
|
new_student.update(
|
||||||
{
|
{
|
||||||
"member": frappe.session.user,
|
"payment": payment_doc.name,
|
||||||
"batch": batch,
|
"source": payment_doc.source,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
new_student.save()
|
||||||
if payment_name:
|
|
||||||
payment = frappe.db.get_value("LMS Payment", payment_name, ["name", "source"], as_dict=True)
|
|
||||||
new_student.update(
|
|
||||||
{
|
|
||||||
"payment": payment.name,
|
|
||||||
"source": payment.source,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
new_student.save()
|
|
||||||
|
|
||||||
|
|
||||||
def update_certificate_purchase(course, payment_name):
|
def update_certificate_purchase(course, payment_name):
|
||||||
@@ -2159,8 +2205,8 @@ def get_program_details(program_name):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def enroll_in_program(program):
|
def enroll_in_program(program):
|
||||||
if frappe.session.user == "Guest":
|
validate_program_enrollment(program)
|
||||||
frappe.throw(_("Please login to enroll in the program."))
|
|
||||||
if not frappe.db.exists("LMS Program Member", {"parent": program, "member": frappe.session.user}):
|
if not frappe.db.exists("LMS Program Member", {"parent": program, "member": frappe.session.user}):
|
||||||
program_member = frappe.new_doc("LMS Program Member")
|
program_member = frappe.new_doc("LMS Program Member")
|
||||||
program_member.update(
|
program_member.update(
|
||||||
@@ -2174,8 +2220,17 @@ def enroll_in_program(program):
|
|||||||
program_member.save(ignore_permissions=True)
|
program_member.save(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_program_enrollment(program):
|
||||||
|
if frappe.session.user == "Guest":
|
||||||
|
frappe.throw(_("Please login to enroll in the program."))
|
||||||
|
|
||||||
|
published = frappe.db.get_value("LMS Program", program, "published")
|
||||||
|
if not published:
|
||||||
|
frappe.throw(_("You cannot enroll in an unpublished program."))
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@rate_limit(limit=50, seconds=60 * 60)
|
@rate_limit(limit=500, seconds=60 * 60)
|
||||||
def get_batches(filters=None, start=0, order_by="start_date"):
|
def get_batches(filters=None, start=0, order_by="start_date"):
|
||||||
if not filters:
|
if not filters:
|
||||||
filters = {}
|
filters = {}
|
||||||
@@ -2289,7 +2344,7 @@ def get_palette(full_name):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
@rate_limit(limit=50, seconds=60 * 60)
|
@rate_limit(limit=500, seconds=60 * 60)
|
||||||
def get_related_courses(course):
|
def get_related_courses(course):
|
||||||
related_course_details = []
|
related_course_details = []
|
||||||
related_courses = frappe.get_all("Related Courses", {"parent": course}, order_by="idx", pluck="course")
|
related_courses = frappe.get_all("Related Courses", {"parent": course}, order_by="idx", pluck="course")
|
||||||
@@ -2638,3 +2693,48 @@ def get_streak_info():
|
|||||||
"current_streak": current_streak,
|
"current_streak": current_streak,
|
||||||
"longest_streak": longest_streak,
|
"longest_streak": longest_streak,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def validate_discussion_reply(doc, method):
|
||||||
|
topic = frappe.db.get_value(
|
||||||
|
"Discussion Topic", doc.topic, ["reference_doctype", "reference_docname"], as_dict=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if topic.reference_doctype == "Course Lesson":
|
||||||
|
validate_course_access(topic.reference_docname)
|
||||||
|
|
||||||
|
elif topic.reference_doctype == "LMS Batch":
|
||||||
|
validate_batch_access(topic.reference_docname)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_course_access(lesson):
|
||||||
|
if not frappe.db.exists("Course Lesson", lesson):
|
||||||
|
frappe.throw(_("The lesson does not exist."))
|
||||||
|
|
||||||
|
if has_moderator_role():
|
||||||
|
return
|
||||||
|
|
||||||
|
if has_course_instructor_role():
|
||||||
|
return
|
||||||
|
|
||||||
|
course = frappe.db.get_value("Course Lesson", lesson, "course")
|
||||||
|
enrollment_exists = frappe.db.exists("LMS Enrollment", {"member": frappe.session.user, "course": course})
|
||||||
|
if not enrollment_exists:
|
||||||
|
frappe.throw(_("You do not have access to this course."))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_batch_access(batch):
|
||||||
|
if not frappe.db.exists("LMS Batch", batch):
|
||||||
|
frappe.throw(_("The batch does not exist."))
|
||||||
|
|
||||||
|
if has_moderator_role():
|
||||||
|
return
|
||||||
|
|
||||||
|
if has_evaluator_role():
|
||||||
|
return
|
||||||
|
|
||||||
|
enrollment_exists = frappe.db.exists(
|
||||||
|
"LMS Batch Enrollment", {"member": frappe.session.user, "batch": batch}
|
||||||
|
)
|
||||||
|
if not enrollment_exists:
|
||||||
|
frappe.throw(_("You do not have access to this batch."))
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"app": "lms",
|
"app": "lms",
|
||||||
"charts": [
|
"charts": [
|
||||||
|
{
|
||||||
|
"chart_name": "Certification",
|
||||||
|
"label": "Certification"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"chart_name": "New Signups",
|
"chart_name": "New Signups",
|
||||||
"label": "Signups"
|
"label": "Signups"
|
||||||
@@ -10,7 +14,7 @@
|
|||||||
"label": "Enrollments"
|
"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 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}}]",
|
"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\":\"_HkvT3xKVi\",\"type\":\"chart\",\"data\":{\"chart_name\":\"Certification\",\"col\":12}},{\"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",
|
"creation": "2021-10-21 17:20:01.358903",
|
||||||
"custom_blocks": [],
|
"custom_blocks": [],
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
@@ -146,8 +150,8 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2024-11-21 12:16:25.886431",
|
"modified": "2025-12-08 13:23:09.718683",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "sayali@frappe.io",
|
||||||
"module": "LMS",
|
"module": "LMS",
|
||||||
"name": "LMS",
|
"name": "LMS",
|
||||||
"number_cards": [],
|
"number_cards": [],
|
||||||
@@ -215,4 +219,4 @@
|
|||||||
],
|
],
|
||||||
"title": "LMS",
|
"title": "LMS",
|
||||||
"type": "Workspace"
|
"type": "Workspace"
|
||||||
}
|
}
|
||||||
|
|||||||
788
lms/locale/ar.po
788
lms/locale/ar.po
File diff suppressed because it is too large
Load Diff
774
lms/locale/bs.po
774
lms/locale/bs.po
File diff suppressed because it is too large
Load Diff
746
lms/locale/cs.po
746
lms/locale/cs.po
File diff suppressed because it is too large
Load Diff
748
lms/locale/da.po
748
lms/locale/da.po
File diff suppressed because it is too large
Load Diff
750
lms/locale/de.po
750
lms/locale/de.po
File diff suppressed because it is too large
Load Diff
748
lms/locale/eo.po
748
lms/locale/eo.po
File diff suppressed because it is too large
Load Diff
796
lms/locale/es.po
796
lms/locale/es.po
File diff suppressed because it is too large
Load Diff
792
lms/locale/fa.po
792
lms/locale/fa.po
File diff suppressed because it is too large
Load Diff
784
lms/locale/fr.po
784
lms/locale/fr.po
File diff suppressed because it is too large
Load Diff
932
lms/locale/hr.po
932
lms/locale/hr.po
File diff suppressed because it is too large
Load Diff
778
lms/locale/hu.po
778
lms/locale/hu.po
File diff suppressed because it is too large
Load Diff
748
lms/locale/id.po
748
lms/locale/id.po
File diff suppressed because it is too large
Load Diff
752
lms/locale/it.po
752
lms/locale/it.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
746
lms/locale/my.po
746
lms/locale/my.po
File diff suppressed because it is too large
Load Diff
782
lms/locale/nb.po
782
lms/locale/nb.po
File diff suppressed because it is too large
Load Diff
746
lms/locale/nl.po
746
lms/locale/nl.po
File diff suppressed because it is too large
Load Diff
748
lms/locale/pl.po
748
lms/locale/pl.po
File diff suppressed because it is too large
Load Diff
746
lms/locale/pt.po
746
lms/locale/pt.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2266
lms/locale/ru.po
2266
lms/locale/ru.po
File diff suppressed because it is too large
Load Diff
8099
lms/locale/sl.po
Normal file
8099
lms/locale/sl.po
Normal file
File diff suppressed because it is too large
Load Diff
748
lms/locale/sr.po
748
lms/locale/sr.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
756
lms/locale/sv.po
756
lms/locale/sv.po
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user