Merge pull request #1417 from frappe/develop
chore: merge 'develop' into 'main'
This commit is contained in:
32
.github/workflows/linters.yml
vendored
32
.github/workflows/linters.yml
vendored
@@ -7,8 +7,27 @@ on:
|
||||
branches: [ main ]
|
||||
|
||||
jobs:
|
||||
commit-lint:
|
||||
name: 'Semantic Commits'
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 200
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
check-latest: true
|
||||
|
||||
- name: Check commit titles
|
||||
run: |
|
||||
npm install @commitlint/cli @commitlint/config-conventional
|
||||
npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
linters:
|
||||
name: Semantic Commits
|
||||
name: Semgrep Rules
|
||||
runs-on: ubuntu-latest
|
||||
if: github.event_name == 'pull_request'
|
||||
|
||||
@@ -20,8 +39,17 @@ jobs:
|
||||
with:
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Cache pip
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v2.0.3
|
||||
uses: pre-commit/action@v3.0.1
|
||||
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
4
.github/workflows/ui-tests.yml
vendored
4
.github/workflows/ui-tests.yml
vendored
@@ -70,7 +70,7 @@ jobs:
|
||||
id: yarn-cache-dir-path
|
||||
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
|
||||
|
||||
- uses: actions/cache@v3
|
||||
- uses: actions/cache@v4
|
||||
id: yarn-cache
|
||||
with:
|
||||
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
|
||||
@@ -79,7 +79,7 @@ jobs:
|
||||
${{ runner.os }}-yarn-ui-
|
||||
|
||||
- name: Cache cypress binary
|
||||
uses: actions/cache@v3
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/Cypress
|
||||
key: ${{ runner.os }}-cypress
|
||||
|
||||
26
commitlint.config.js
Normal file
26
commitlint.config.js
Normal file
@@ -0,0 +1,26 @@
|
||||
module.exports = {
|
||||
parserPreset: "conventional-changelog-conventionalcommits",
|
||||
rules: {
|
||||
"subject-empty": [2, "never"],
|
||||
"type-case": [2, "always", "lower-case"],
|
||||
"type-empty": [2, "never"],
|
||||
"type-enum": [
|
||||
2,
|
||||
"always",
|
||||
[
|
||||
"build",
|
||||
"chore",
|
||||
"ci",
|
||||
"docs",
|
||||
"feat",
|
||||
"fix",
|
||||
"perf",
|
||||
"refactor",
|
||||
"revert",
|
||||
"style",
|
||||
"test",
|
||||
"deprecate", // deprecation decision
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
@@ -2,9 +2,9 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" href="{{ favicon or '/assets/lms/frontend/favicon.png' }}" />
|
||||
<link rel="icon" href="{{ favicon }}" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Frappe Learning</title>
|
||||
<title>{{ title }}</title>
|
||||
<meta name="title" content="{{ meta.title }}" />
|
||||
<meta name="image" content="{{ meta.image }}" />
|
||||
<meta name="description" content="{{ meta.description }}" />
|
||||
@@ -23,17 +23,6 @@
|
||||
<p>
|
||||
{{ meta.description }}
|
||||
</p>
|
||||
<p>
|
||||
The content here is just for seo purposes. The actual content will be loaded in a few seconds.
|
||||
</p>
|
||||
<p>
|
||||
Seo checks if a page has more than 300 words. So, here are some more words to make it more than 300 words.
|
||||
Page descriptions are the HTML meta tags that provide a brief summary of a web page.
|
||||
Search engines use meta descriptions to help identify the page's topic - they don't use them to rank the page, but they do use them to determine whether or not to display the page in search results.
|
||||
Meta descriptions are important because they're often the first thing people see when they're deciding which search result to click on.
|
||||
They're also important because they can help improve your click-through rate (CTR) from search results.
|
||||
A good meta description can entice people to click on your page instead of someone else's.
|
||||
</p>
|
||||
<a href="{{ meta.link }}">Know More</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -41,9 +30,8 @@
|
||||
<div id="popovers"></div>
|
||||
|
||||
<script>
|
||||
window.csrf_token = '{{ csrf_token }}'
|
||||
window.setup_complete = '{{ setup_complete }}'
|
||||
document.getElementById('seo-content').style.display = 'none';
|
||||
window.csrf_token = '{{ csrf_token }}'
|
||||
</script>
|
||||
<script type="module" src="/src/main.js"></script>
|
||||
</body>
|
||||
|
||||
4
frontend/public/learning.svg
Normal file
4
frontend/public/learning.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="80" height="79" viewBox="0 0 80 79" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M57.1285 0.580383H22.8514C10.2309 0.580383 0 10.5649 0 22.8815V56.3332C0 68.6497 10.2309 78.6343 22.8514 78.6343H57.1285C69.749 78.6343 79.9799 68.6497 79.9799 56.3332V22.8815C79.9799 10.5649 69.749 0.580383 57.1285 0.580383Z" fill="#0E7159"/>
|
||||
<path d="M62.8434 23.6906L60.7869 23.1052C53.6744 21.0702 45.9048 22.4641 39.992 26.8128C35.8502 23.7742 30.7943 22.1854 25.7099 22.2133H17.1406V27.8163H25.7099C29.6232 27.8163 33.508 29.015 36.6787 31.3845L39.992 33.8377L43.3056 31.3845C47.2475 28.4575 52.3032 27.2588 57.1306 28.0393V50.647C51.1035 49.9223 44.9051 51.4834 39.992 55.0795C35.8502 52.0688 30.8515 50.4798 25.7671 50.4798C24.7959 50.4798 23.8247 50.5355 22.8535 50.647V35.0642H17.1406V57.0588H62.8434V23.7185V23.6906Z" fill="white"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 856 B |
@@ -146,7 +146,6 @@ function resetEditor(value: string, resetHistory = false) {
|
||||
value = getModelValue()
|
||||
aceEditor?.setValue(value)
|
||||
aceEditor?.clearSelection()
|
||||
console.log(isDark.value)
|
||||
aceEditor?.setTheme(isDark.value ? 'ace/theme/twilight' : 'ace/theme/chrome')
|
||||
props.autofocus && aceEditor?.focus()
|
||||
if (resetHistory) {
|
||||
|
||||
@@ -142,6 +142,7 @@
|
||||
</div>
|
||||
</div>
|
||||
<ChapterModal
|
||||
v-if="user.data"
|
||||
v-model="showChapterModal"
|
||||
v-model:outline="outline"
|
||||
:course="courseName"
|
||||
|
||||
@@ -1,36 +1,18 @@
|
||||
<template>
|
||||
<svg
|
||||
width="118"
|
||||
height="118"
|
||||
viewBox="0 0 118 118"
|
||||
width="80"
|
||||
height="79"
|
||||
viewBox="0 0 80 79"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M93.9278 0H23.1013C10.3428 0 0 10.3428 0 23.1013V93.9278C0 106.686 10.3428 117.029 23.1013 117.029H93.9278C106.686 117.029 117.029 106.686 117.029 93.9278V23.1013C117.029 10.3428 106.686 0 93.9278 0Z"
|
||||
fill="url(#paint0_radial_174_336)"
|
||||
d="M57.1285 0.580383H22.8514C10.2309 0.580383 0 10.5649 0 22.8815V56.3332C0 68.6497 10.2309 78.6343 22.8514 78.6343H57.1285C69.749 78.6343 79.9799 68.6497 79.9799 56.3332V22.8815C79.9799 10.5649 69.749 0.580383 57.1285 0.580383Z"
|
||||
fill="#0E7159"
|
||||
/>
|
||||
<path
|
||||
d="M93.9278 0H23.1013C10.3428 0 0 10.3428 0 23.1013V93.9278C0 106.686 10.3428 117.029 23.1013 117.029H93.9278C106.686 117.029 117.029 106.686 117.029 93.9278V23.1013C117.029 10.3428 106.686 0 93.9278 0Z"
|
||||
fill="#0B3D3D"
|
||||
fill-opacity="0.8"
|
||||
d="M62.8434 23.6906L60.7869 23.1052C53.6744 21.0702 45.9048 22.4641 39.992 26.8128C35.8502 23.7742 30.7943 22.1854 25.7099 22.2133H17.1406V27.8163H25.7099C29.6232 27.8163 33.508 29.015 36.6787 31.3845L39.992 33.8377L43.3056 31.3845C47.2475 28.4575 52.3032 27.2588 57.1306 28.0393V50.647C51.1035 49.9223 44.9051 51.4834 39.992 55.0795C35.8502 52.0688 30.8515 50.4798 25.7671 50.4798C24.7959 50.4798 23.8247 50.5355 22.8535 50.647V35.0642H17.1406V57.0588H62.8434V23.7185V23.6906Z"
|
||||
fill="white"
|
||||
/>
|
||||
<path
|
||||
d="M95.1879 33.1294L91.4077 32.0268C80.1721 28.7716 67.9389 30.9242 58.5409 37.7496C52.083 33.0769 43.9975 30.5042 36.1746 30.5042H21.8938V41.0048H36.2796C42.2649 41.0048 48.1978 42.9999 52.923 46.6226L58.5934 50.9279L64.2637 46.6226C70.144 42.1599 77.5469 40.2698 84.7923 41.2673V76.1818C75.5518 75.2367 66.2063 77.7044 58.6459 83.2172C51.0854 77.7044 41.6349 75.2367 32.4994 76.1818V52.8705H21.9988V86.4724H95.3454V33.1294H95.1879Z"
|
||||
fill="#58FF9B"
|
||||
/>
|
||||
<defs>
|
||||
<radialGradient
|
||||
id="paint0_radial_174_336"
|
||||
cx="0"
|
||||
cy="0"
|
||||
r="1"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="translate(117.24 -101.5) rotate(105.042) scale(226.282)"
|
||||
>
|
||||
<stop offset="0.445162" stop-color="#1F7676" />
|
||||
<stop offset="1" stop-color="#0A4B4B" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</template>
|
||||
|
||||
@@ -1,41 +1,37 @@
|
||||
<template>
|
||||
<div class="flex space-x-4 border rounded-md p-2">
|
||||
<img :src="job.company_logo" class="size-10 rounded-full object-contain" />
|
||||
<div class="flex flex-col space-y-2 flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-semibold text-ink-gray-9">
|
||||
{{ job.job_title }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 text-ink-gray-5">
|
||||
<Building2 class="w-4 h-4 stroke-1.5" />
|
||||
<span>
|
||||
<div class="border rounded-md p-4">
|
||||
<div class="flex space-x-4">
|
||||
<img
|
||||
:src="job.company_logo"
|
||||
class="size-10 rounded-full object-contain"
|
||||
/>
|
||||
<div class="flex flex-col space-y-1 flex-1">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-lg font-semibold text-ink-gray-9">
|
||||
{{ job.job_title }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-xs text-ink-gray-5">
|
||||
{{ job.company_name }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 text-ink-gray-5">
|
||||
<MapPin class="w-4 h-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ job.location }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 text-ink-gray-5">
|
||||
<Shapes class="w-4 h-4 stroke-1.5" />
|
||||
<span>
|
||||
{{ job.type }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-2 text-ink-gray-5">
|
||||
<Calendar class="w-4 h-4 stroke-1.5" />
|
||||
<span> {{ __('posted') }} {{ dayjs(job.creation).fromNow() }} </span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-x-4 mt-2">
|
||||
<Badge>
|
||||
{{ job.location }}
|
||||
</Badge>
|
||||
<Badge>
|
||||
{{ job.type }}
|
||||
</Badge>
|
||||
<Badge>
|
||||
{{ dayjs(job.creation).fromNow() }}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Building2, Calendar, MapPin, Shapes } from 'lucide-vue-next'
|
||||
import { inject } from 'vue'
|
||||
import { Avatar } from 'frappe-ui'
|
||||
import { Badge } from 'frappe-ui'
|
||||
|
||||
const dayjs = inject('$dayjs')
|
||||
const props = defineProps({
|
||||
|
||||
@@ -119,7 +119,7 @@ const { updateOnboardingStep } = useOnboarding('learning')
|
||||
|
||||
const existingQuestion = reactive({
|
||||
question: '',
|
||||
marks: 0,
|
||||
marks: 1,
|
||||
})
|
||||
const question = reactive({
|
||||
question: '',
|
||||
|
||||
@@ -328,19 +328,26 @@ const tabsStructure = computed(() => {
|
||||
icon: 'LogIn',
|
||||
fields: [
|
||||
{
|
||||
label: 'Custom Content',
|
||||
label: 'Identify User Persona',
|
||||
name: 'user_category',
|
||||
type: 'checkbox',
|
||||
description:
|
||||
'Enable this option to identify the user persona during signup.',
|
||||
},
|
||||
{
|
||||
label: 'Disable signup',
|
||||
name: 'disable_signup',
|
||||
type: 'checkbox',
|
||||
description:
|
||||
'New users will have to be manually registered by Admins.',
|
||||
},
|
||||
{
|
||||
label: 'Signup Consent HTML',
|
||||
name: 'custom_signup_content',
|
||||
type: 'Code',
|
||||
mode: 'htmlmixed',
|
||||
rows: 10,
|
||||
},
|
||||
{
|
||||
label: 'Ask for Occupation',
|
||||
name: 'user_category',
|
||||
type: 'checkbox',
|
||||
description:
|
||||
'Enable this option to ask users to select their occupation during the signup process.',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<div v-if="quiz.data">
|
||||
<div
|
||||
class="bg-surface-blue-2 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-ink-blue-2"
|
||||
class="bg-surface-blue-2 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-ink-blue-3"
|
||||
>
|
||||
<div class="leading-5">
|
||||
{{
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { FormControl, FileUploader, Button, Switch } from 'frappe-ui'
|
||||
import { computed } from 'vue'
|
||||
import { computed, onMounted } from 'vue'
|
||||
import { getFileSize, validateFile } from '@/utils'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<span v-else> Learning </span>
|
||||
</div>
|
||||
<div
|
||||
v-if="userResource"
|
||||
v-if="userResource.data"
|
||||
class="mt-1 text-sm text-ink-gray-7 leading-none"
|
||||
>
|
||||
{{ convertToTitleCase(userResource.data?.full_name) }}
|
||||
|
||||
@@ -63,6 +63,7 @@ import {
|
||||
createResource,
|
||||
FormControl,
|
||||
TextEditor,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
computed,
|
||||
@@ -72,11 +73,13 @@ import {
|
||||
reactive,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { showToast } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const user = inject('$user')
|
||||
const router = useRouter()
|
||||
const { brand } = sessionStore()
|
||||
|
||||
const props = defineProps({
|
||||
assignmentID: {
|
||||
@@ -188,4 +191,11 @@ const assignmentOptions = computed(() => {
|
||||
{ label: 'URL', value: 'URL' },
|
||||
]
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: assignment.doc ? assignment.doc.title : __('New Assignment'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Breadcrumbs, createResource } from 'frappe-ui'
|
||||
import { computed, inject, onMounted, onBeforeUnmount, ref } from 'vue'
|
||||
import { Breadcrumbs, createResource, usePageMeta } from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import Assignment from '@/components/Assignment.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const fromLesson = ref(false)
|
||||
const { brand } = sessionStore()
|
||||
|
||||
const props = defineProps({
|
||||
assignmentID: {
|
||||
@@ -72,4 +74,11 @@ const breadcrumbs = computed(() => {
|
||||
]
|
||||
return crumbs
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: title.data?.title,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -84,14 +84,17 @@ import {
|
||||
ListRows,
|
||||
ListRow,
|
||||
ListRowItem,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { Pencil } from 'lucide-vue-next'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
const { brand } = sessionStore()
|
||||
const router = useRouter()
|
||||
const assignmentID = ref('')
|
||||
const member = ref('')
|
||||
@@ -214,4 +217,11 @@ const breadcrumbs = computed(() => {
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: __('Assignment Submissions'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -80,15 +80,18 @@ import {
|
||||
createListResource,
|
||||
FormControl,
|
||||
ListView,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import { Plus, Pencil } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { sessionStore } from '../stores/session'
|
||||
|
||||
const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
const titleFilter = ref('')
|
||||
const typeFilter = ref('')
|
||||
const { brand } = sessionStore()
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(() => {
|
||||
@@ -184,4 +187,11 @@ const breadcrumbs = computed(() => [
|
||||
route: { name: 'Assignments' },
|
||||
},
|
||||
])
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: __('Assignments'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -24,10 +24,12 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createDocumentResource, createResource } from 'frappe-ui'
|
||||
import { createResource, usePageMeta } from 'frappe-ui'
|
||||
import { computed, inject } from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
|
||||
const dayjs = inject('$dayjs')
|
||||
const { brand } = sessionStore()
|
||||
|
||||
const props = defineProps({
|
||||
badgeName: {
|
||||
@@ -70,4 +72,11 @@ const breadcrumbs = computed(() => {
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: badge.data.badge,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -199,9 +199,14 @@
|
||||
<script setup>
|
||||
import { computed, inject, ref, onMounted, watch } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { Breadcrumbs, Button, createResource, Tabs, Badge } from 'frappe-ui'
|
||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
createResource,
|
||||
Tabs,
|
||||
Badge,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
Clock,
|
||||
LayoutDashboard,
|
||||
@@ -214,7 +219,10 @@ import {
|
||||
Globe,
|
||||
ClipboardPen,
|
||||
} from 'lucide-vue-next'
|
||||
import { formatTime, updateDocumentTitle } from '@/utils'
|
||||
import { formatTime } from '@/utils'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import BatchDashboard from '@/components/BatchDashboard.vue'
|
||||
import BatchCourses from '@/components/BatchCourses.vue'
|
||||
import LiveClass from '@/components/LiveClass.vue'
|
||||
@@ -232,6 +240,7 @@ const showAnnouncementModal = ref(false)
|
||||
const openCertificateDialog = ref(false)
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const { brand } = sessionStore()
|
||||
const tabIndex = ref(0)
|
||||
|
||||
const tabs = computed(() => {
|
||||
@@ -345,12 +354,10 @@ watch(tabIndex, () => {
|
||||
}
|
||||
})
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: batch.data?.title,
|
||||
description: batch.data?.description,
|
||||
title: batch?.data?.title,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
|
||||
@@ -102,8 +102,9 @@
|
||||
import { computed, inject } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { BookOpen, Clock } from 'lucide-vue-next'
|
||||
import { formatTime, updateDocumentTitle } from '@/utils'
|
||||
import { Breadcrumbs, createResource } from 'frappe-ui'
|
||||
import { formatTime } from '@/utils'
|
||||
import { Breadcrumbs, createResource, usePageMeta } from 'frappe-ui'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import CourseCard from '@/components/CourseCard.vue'
|
||||
import BatchOverlay from '@/components/BatchOverlay.vue'
|
||||
import DateRange from '../components/Common/DateRange.vue'
|
||||
@@ -112,6 +113,7 @@ import UserAvatar from '@/components/UserAvatar.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const router = useRouter()
|
||||
const { brand } = sessionStore()
|
||||
|
||||
const props = defineProps({
|
||||
batchName: {
|
||||
@@ -152,14 +154,12 @@ const breadcrumbs = computed(() => {
|
||||
return items
|
||||
})
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: batch.data?.title,
|
||||
description: batch.data?.description,
|
||||
title: batch?.data?.title,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
<style>
|
||||
.batch-description p {
|
||||
|
||||
@@ -264,17 +264,20 @@ import {
|
||||
Button,
|
||||
TextEditor,
|
||||
createResource,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast } from '@/utils'
|
||||
import { Image } from 'lucide-vue-next'
|
||||
import { capture } from '@/telemetry'
|
||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const user = inject('$user')
|
||||
const { brand } = sessionStore()
|
||||
const { updateOnboardingStep } = useOnboarding('learning')
|
||||
|
||||
const props = defineProps({
|
||||
@@ -505,4 +508,11 @@ const breadcrumbs = computed(() => {
|
||||
})
|
||||
return crumbs
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: props.batchName == 'new' ? 'New Batch' : batchDetail.data?.title,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -104,14 +104,16 @@ import {
|
||||
FormControl,
|
||||
Select,
|
||||
TabButtons,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import { BookOpen, Plus } from 'lucide-vue-next'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import BatchCard from '@/components/BatchCard.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
const { brand } = sessionStore()
|
||||
const start = ref(0)
|
||||
const pageLength = ref(20)
|
||||
const categories = ref([])
|
||||
@@ -304,12 +306,10 @@ const breadcrumbs = computed(() => [
|
||||
},
|
||||
])
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: 'Batches',
|
||||
description: 'All upcoming batches.',
|
||||
title: __('Batches'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
|
||||
@@ -151,19 +151,20 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
Input,
|
||||
Button,
|
||||
createResource,
|
||||
FormControl,
|
||||
Breadcrumbs,
|
||||
Tooltip,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { reactive, inject, onMounted, computed } from 'vue'
|
||||
import { showToast } from '@/utils/'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import NotPermitted from '@/components/NotPermitted.vue'
|
||||
import { showToast } from '@/utils/'
|
||||
|
||||
const user = inject('$user')
|
||||
const { brand } = sessionStore()
|
||||
|
||||
onMounted(() => {
|
||||
const script = document.createElement('script')
|
||||
@@ -356,4 +357,11 @@ const redirectTo = computed(() => {
|
||||
return `/lms/courses/${props.name}/certification`
|
||||
}
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: __('Billing Details'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -102,14 +102,17 @@ import {
|
||||
createListResource,
|
||||
FormControl,
|
||||
Select,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
import { BookOpen, GraduationCap } from 'lucide-vue-next'
|
||||
import { sessionStore } from '../stores/session'
|
||||
|
||||
const currentCategory = ref('')
|
||||
const filters = ref({})
|
||||
const nameFilter = ref('')
|
||||
const { brand } = sessionStore()
|
||||
|
||||
onMounted(() => {
|
||||
updateParticipants()
|
||||
@@ -163,13 +166,12 @@ const breadcrumbs = computed(() => [
|
||||
},
|
||||
])
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: 'Certified Participants',
|
||||
description: 'All participants that have been certified.',
|
||||
title: __('Certified Participants'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
<style>
|
||||
.headline {
|
||||
|
||||
@@ -36,12 +36,14 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { Breadcrumbs, call, createResource } from 'frappe-ui'
|
||||
import { Breadcrumbs, call, createResource, usePageMeta } from 'frappe-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
|
||||
|
||||
const courseTitle = ref(null)
|
||||
const evaluator = ref(null)
|
||||
const { brand } = sessionStore()
|
||||
const courses = ref([])
|
||||
const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
@@ -133,4 +135,11 @@ const breadcrumbs = computed(() => [
|
||||
label: __('Certification'),
|
||||
},
|
||||
])
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: courseTitle.value,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -92,16 +92,24 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createResource, Breadcrumbs, Badge, Tooltip } from 'frappe-ui'
|
||||
import {
|
||||
createResource,
|
||||
Breadcrumbs,
|
||||
Badge,
|
||||
Tooltip,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed } from 'vue'
|
||||
import { Users, Star } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import CourseCardOverlay from '@/components/CourseCardOverlay.vue'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
import CourseReviews from '@/components/CourseReviews.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
|
||||
const props = defineProps({
|
||||
courseName: {
|
||||
type: String,
|
||||
@@ -127,14 +135,12 @@ const breadcrumbs = computed(() => {
|
||||
return items
|
||||
})
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: course?.data?.title,
|
||||
description: course?.data?.short_introduction,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
<style>
|
||||
.avatar-group {
|
||||
|
||||
@@ -253,6 +253,7 @@ import {
|
||||
createResource,
|
||||
FormControl,
|
||||
FileUploader,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
inject,
|
||||
@@ -265,17 +266,19 @@ import {
|
||||
getCurrentInstance,
|
||||
} from 'vue'
|
||||
import { showToast, updateDocumentTitle } from '@/utils'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { Image, Trash2, X } from 'lucide-vue-next'
|
||||
import { useRouter } from 'vue-router'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||
import { capture } from '@/telemetry'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
import MultiSelect from '@/components/Controls/MultiSelect.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
const newTag = ref('')
|
||||
const { brand } = sessionStore()
|
||||
const router = useRouter()
|
||||
const instructors = ref([])
|
||||
const settingsStore = useSettings()
|
||||
@@ -574,12 +577,10 @@ const breadcrumbs = computed(() => {
|
||||
return crumbs
|
||||
})
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: 'Create a Course',
|
||||
description: 'Create or edit a course for your learning system.',
|
||||
title: courseResource.data?.title || __('New Course'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
|
||||
@@ -100,10 +100,11 @@ import {
|
||||
FormControl,
|
||||
Select,
|
||||
TabButtons,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref, watch } from 'vue'
|
||||
import { BookOpen, Plus } from 'lucide-vue-next'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import CourseCard from '@/components/CourseCard.vue'
|
||||
|
||||
const user = inject('$user')
|
||||
@@ -116,6 +117,7 @@ const title = ref('')
|
||||
const certification = ref(false)
|
||||
const filters = ref({})
|
||||
const currentTab = ref('Live')
|
||||
const { brand } = sessionStore()
|
||||
|
||||
onMounted(() => {
|
||||
setFiltersFromQuery()
|
||||
@@ -303,12 +305,10 @@ const breadcrumbs = computed(() => [
|
||||
},
|
||||
])
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: 'Courses',
|
||||
description: 'All published courses.',
|
||||
title: __('Courses'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
<template>
|
||||
<div class="max-w-3xl py-12 mx-auto">
|
||||
<Button
|
||||
icon-left="code"
|
||||
@click="$resources.ping.fetch"
|
||||
:loading="$resources.ping.loading"
|
||||
>
|
||||
Click to send 'ping' request
|
||||
</Button>
|
||||
<div>
|
||||
{{ $resources.ping.data }}
|
||||
</div>
|
||||
<pre>{{ $resources.ping }}</pre>
|
||||
|
||||
<Button @click="showDialog = true">Open Dialog</Button>
|
||||
<Dialog title="Title" v-model="showDialog"> Dialog content </Dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dialog } from 'frappe-ui'
|
||||
|
||||
export default {
|
||||
name: 'Home',
|
||||
data() {
|
||||
return {
|
||||
showDialog: false,
|
||||
}
|
||||
},
|
||||
resources: {
|
||||
ping: {
|
||||
url: 'ping',
|
||||
},
|
||||
},
|
||||
components: {
|
||||
Dialog,
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@@ -139,14 +139,17 @@ import {
|
||||
Button,
|
||||
TextEditor,
|
||||
FileUploader,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, onMounted, reactive, inject } from 'vue'
|
||||
import { FileText, X } from 'lucide-vue-next'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { getFileSize, showToast } from '../utils'
|
||||
|
||||
const user = inject('$user')
|
||||
const router = useRouter()
|
||||
const { brand } = sessionStore()
|
||||
|
||||
const props = defineProps({
|
||||
jobName: {
|
||||
@@ -319,4 +322,11 @@ const breadcrumbs = computed(() => {
|
||||
]
|
||||
return crumbs
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: props.jobName == 'new' ? 'New Job' : jobDetail.data?.title,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<div v-if="user.data?.name" class="flex">
|
||||
<div v-if="user.data?.name" class="flex space-x-2">
|
||||
<router-link
|
||||
v-if="user.data.name == job.data?.owner"
|
||||
:to="{
|
||||
@@ -24,13 +24,19 @@
|
||||
params: { jobName: job.data?.name },
|
||||
}"
|
||||
>
|
||||
<Button class="mr-2">
|
||||
<Button>
|
||||
<template #prefix>
|
||||
<Pencil class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Edit') }}
|
||||
</Button>
|
||||
</router-link>
|
||||
<Button @click="redirectToWebsite(job.data?.company_website)">
|
||||
<template #prefix>
|
||||
<SquareArrowOutUpRight class="h-4 w-4 stroke-1.5" />
|
||||
</template>
|
||||
{{ __('Visit Website') }}
|
||||
</Button>
|
||||
<Button
|
||||
v-if="!jobApplication.data?.length"
|
||||
variant="solid"
|
||||
@@ -56,10 +62,11 @@
|
||||
<div class="flex items-center">
|
||||
<img
|
||||
:src="job.data.company_logo"
|
||||
class="w-16 h-16 rounded-lg object-contain mr-4"
|
||||
class="w-16 h-16 rounded-lg object-contain cursor-pointer mr-4"
|
||||
:alt="job.data.company_name"
|
||||
@click="redirectToWebsite(job.data.company_website)"
|
||||
/>
|
||||
<div class="text-2xl text-ink-gray-9 font-semibold mb-4">
|
||||
<div class="text-2xl text-ink-gray-9 font-semibold">
|
||||
{{ job.data.job_title }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -69,7 +76,7 @@
|
||||
>
|
||||
<div class="flex items-center space-x-4">
|
||||
<Building2 class="h-4 w-4 text-ink-green-2" />
|
||||
<div class="flex flex-col space-y-2 text-ink-gray-7">
|
||||
<div class="flex flex-col space-y-1 text-ink-gray-7">
|
||||
<span class="text-xs text-ink-gray-5 font-medium uppercase">
|
||||
{{ __('Organisation') }}
|
||||
</span>
|
||||
@@ -80,7 +87,7 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<MapPin class="size-4 text-ink-red-3" />
|
||||
<div class="flex flex-col space-y-2 text-ink-gray-7">
|
||||
<div class="flex flex-col space-y-1 text-ink-gray-7">
|
||||
<span class="text-xs font-medium uppercase">
|
||||
{{ __('Location') }}
|
||||
</span>
|
||||
@@ -91,7 +98,7 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<ClipboardType class="h-4 w-4 text-yellow-500" />
|
||||
<div class="flex flex-col space-y-2 text-ink-gray-7">
|
||||
<div class="flex flex-col space-y-1 text-ink-gray-7">
|
||||
<span class="text-xs font-medium uppercase">
|
||||
{{ __('Category') }}
|
||||
</span>
|
||||
@@ -102,7 +109,7 @@
|
||||
</div>
|
||||
<div class="flex items-center space-x-4">
|
||||
<CalendarDays class="h-4 w-4 text-ink-blue-2" />
|
||||
<div class="flex flex-col space-y-2 text-ink-gray-7">
|
||||
<div class="flex flex-col space-y-1 text-ink-gray-7">
|
||||
<span class="text-xs font-medium uppercase">
|
||||
{{ __('Posted on') }}
|
||||
</span>
|
||||
@@ -116,7 +123,7 @@
|
||||
class="flex items-center space-x-4"
|
||||
>
|
||||
<SquareUserRound class="h-4 w-4 text-purple-500" />
|
||||
<div class="flex flex-col space-y-2 text-ink-gray-7">
|
||||
<div class="flex flex-col space-y-1 text-ink-gray-7">
|
||||
<span class="text-xs font-medium uppercase">
|
||||
{{ __('Applications Received') }}
|
||||
</span>
|
||||
@@ -142,9 +149,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Button, Breadcrumbs, createResource } from 'frappe-ui'
|
||||
import { inject, ref, computed } from 'vue'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
import { Button, Breadcrumbs, createResource, usePageMeta } from 'frappe-ui'
|
||||
import { inject, ref } from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import JobApplicationModal from '@/components/Modals/JobApplicationModal.vue'
|
||||
import {
|
||||
MapPin,
|
||||
@@ -154,10 +161,12 @@ import {
|
||||
CalendarDays,
|
||||
ClipboardType,
|
||||
SquareUserRound,
|
||||
SquareArrowOutUpRight,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const user = inject('$user')
|
||||
const dayjs = inject('$dayjs')
|
||||
const { brand } = sessionStore()
|
||||
const showApplicationModal = ref(false)
|
||||
|
||||
const props = defineProps({
|
||||
@@ -215,12 +224,14 @@ const redirectToLogin = (job) => {
|
||||
window.location.href = `/login?redirect-to=/job-openings/${job}`
|
||||
}
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
const redirectToWebsite = (url) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: job.data?.job_title,
|
||||
description: job.data?.description,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
</router-link>
|
||||
</header>
|
||||
<div>
|
||||
<div class="lg:w-3/4 mx-auto p-5">
|
||||
<div class="p-5">
|
||||
<div
|
||||
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5"
|
||||
>
|
||||
@@ -60,7 +60,7 @@
|
||||
|
||||
<div
|
||||
v-if="jobs.data?.length"
|
||||
class="grid grid-cols-1 lg:grid-cols-2 gap-5"
|
||||
class="grid grid-cols-1 lg:grid-cols-3 gap-5"
|
||||
>
|
||||
<router-link
|
||||
v-for="job in jobs.data"
|
||||
@@ -81,14 +81,21 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Button, Breadcrumbs, createResource, FormControl } from 'frappe-ui'
|
||||
import {
|
||||
Button,
|
||||
Breadcrumbs,
|
||||
createResource,
|
||||
FormControl,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { Plus, Search } from 'lucide-vue-next'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { inject, computed, ref, onMounted } from 'vue'
|
||||
import JobCard from '@/components/JobCard.vue'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
|
||||
const user = inject('$user')
|
||||
const jobType = ref(null)
|
||||
const { brand } = sessionStore()
|
||||
const searchQuery = ref('')
|
||||
const filters = ref({})
|
||||
const orFilters = ref({})
|
||||
@@ -147,12 +154,11 @@ const jobTypes = computed(() => {
|
||||
{ label: __('Freelance'), value: 'Freelance' },
|
||||
]
|
||||
})
|
||||
const pageMeta = computed(() => {
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: 'Jobs',
|
||||
description: 'An open job board for the community',
|
||||
title: __('Jobs'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
|
||||
@@ -193,14 +193,15 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createResource, Breadcrumbs, Button } from 'frappe-ui'
|
||||
import { createResource, Breadcrumbs, Button, usePageMeta } from 'frappe-ui'
|
||||
import { computed, watch, inject, ref, onMounted, onBeforeUnmount } from 'vue'
|
||||
import CourseOutline from '@/components/CourseOutline.vue'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ChevronLeft, ChevronRight, GraduationCap } from 'lucide-vue-next'
|
||||
import Discussions from '@/components/Discussions.vue'
|
||||
import { getEditorTools, updateDocumentTitle } from '../utils'
|
||||
import { getEditorTools } from '../utils'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import EditorJS from '@editorjs/editorjs'
|
||||
import LessonContent from '@/components/LessonContent.vue'
|
||||
import CourseInstructors from '@/components/CourseInstructors.vue'
|
||||
@@ -215,6 +216,7 @@ const editor = ref(null)
|
||||
const instructorEditor = ref(null)
|
||||
const lessonProgress = ref(0)
|
||||
const timer = ref(0)
|
||||
const { brand } = sessionStore()
|
||||
let timerInterval
|
||||
|
||||
const props = defineProps({
|
||||
@@ -419,14 +421,12 @@ const redirectToLogin = () => {
|
||||
window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}`
|
||||
}
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: lesson.data?.title,
|
||||
description: lesson.data?.course,
|
||||
title: lesson?.data?.title,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
<style>
|
||||
.avatar-group {
|
||||
|
||||
@@ -78,7 +78,13 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Breadcrumbs, Button, createResource, FormControl } from 'frappe-ui'
|
||||
import {
|
||||
Breadcrumbs,
|
||||
Button,
|
||||
createResource,
|
||||
FormControl,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
computed,
|
||||
reactive,
|
||||
@@ -87,13 +93,15 @@ import {
|
||||
ref,
|
||||
onBeforeUnmount,
|
||||
} from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import EditorJS from '@editorjs/editorjs'
|
||||
import LessonHelp from '@/components/LessonHelp.vue'
|
||||
import { ChevronRight } from 'lucide-vue-next'
|
||||
import { updateDocumentTitle, createToast, getEditorTools } from '@/utils'
|
||||
import { createToast, getEditorTools } from '@/utils'
|
||||
import { capture } from '@/telemetry'
|
||||
import { useOnboarding } from 'frappe-ui/frappe'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const editor = ref(null)
|
||||
const instructorEditor = ref(null)
|
||||
const user = inject('$user')
|
||||
@@ -492,14 +500,14 @@ const breadcrumbs = computed(() => {
|
||||
return crumbs
|
||||
})
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: 'Lesson Editor',
|
||||
description: 'Create and edit lessons for your course',
|
||||
title: lessonDetails?.data?.lesson
|
||||
? lessonDetails.data.lesson.title
|
||||
: 'New Lesson',
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
<style>
|
||||
.embed-tool__caption,
|
||||
|
||||
@@ -65,12 +65,14 @@ import {
|
||||
TabButtons,
|
||||
Button,
|
||||
Tooltip,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { computed, inject, ref, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { X } from 'lucide-vue-next'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const user = inject('$user')
|
||||
const socket = inject('$socket')
|
||||
const activeTab = ref('Unread')
|
||||
@@ -145,14 +147,12 @@ const breadcrumbs = computed(() => {
|
||||
return crumbs
|
||||
})
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: 'Notifications',
|
||||
description: 'All your notifications in one place.',
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
<style>
|
||||
.notification strong {
|
||||
|
||||
@@ -86,18 +86,24 @@
|
||||
/>
|
||||
</template>
|
||||
<script setup>
|
||||
import { Breadcrumbs, createResource, Button, TabButtons } from 'frappe-ui'
|
||||
import {
|
||||
Breadcrumbs,
|
||||
createResource,
|
||||
Button,
|
||||
TabButtons,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, watch, ref, onMounted, watchEffect } from 'vue'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
import { Edit } from 'lucide-vue-next'
|
||||
import { Edit, icons } from 'lucide-vue-next'
|
||||
import UserAvatar from '@/components/UserAvatar.vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import NoPermission from '@/components/NoPermission.vue'
|
||||
import { convertToTitleCase, updateDocumentTitle } from '@/utils'
|
||||
import { convertToTitleCase } from '@/utils'
|
||||
import EditProfile from '@/components/Modals/EditProfile.vue'
|
||||
import EditCoverImage from '@/components/Modals/EditCoverImage.vue'
|
||||
|
||||
const { user } = sessionStore()
|
||||
const { user, brand } = sessionStore()
|
||||
const $user = inject('$user')
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
@@ -215,12 +221,10 @@ const breadcrumbs = computed(() => {
|
||||
return crumbs
|
||||
})
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: profile.data?.full_name,
|
||||
description: profile.data?.headline,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
|
||||
@@ -186,14 +186,17 @@ import {
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
ListSelectBanner,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
import { showToast } from '@/utils/'
|
||||
import Draggable from 'vuedraggable'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import Draggable from 'vuedraggable'
|
||||
import Link from '@/components/Controls/Link.vue'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const showDialog = ref(false)
|
||||
const currentForm = ref(null)
|
||||
const course = ref(null)
|
||||
@@ -364,4 +367,11 @@ const breadbrumbs = computed(() => {
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: program.doc?.title,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -126,14 +126,17 @@ import {
|
||||
createResource,
|
||||
Dialog,
|
||||
FormControl,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { BookOpen, Edit, Plus, LockKeyhole } from 'lucide-vue-next'
|
||||
import CourseCard from '@/components/CourseCard.vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { showToast } from '@/utils'
|
||||
import { useSettings } from '@/stores/settings'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const user = inject('$user')
|
||||
const showDialog = ref(false)
|
||||
const router = useRouter()
|
||||
@@ -210,4 +213,11 @@ const breadbrumbs = computed(() => [
|
||||
label: 'Programs',
|
||||
},
|
||||
])
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: __('Programs'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -197,6 +197,7 @@ import {
|
||||
ListRowItem,
|
||||
ListSelectBanner,
|
||||
Button,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import {
|
||||
computed,
|
||||
@@ -207,11 +208,13 @@ import {
|
||||
onBeforeUnmount,
|
||||
watch,
|
||||
} from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { Plus, Trash2 } from 'lucide-vue-next'
|
||||
import Question from '@/components/Modals/Question.vue'
|
||||
import { showToast, updateDocumentTitle } from '@/utils'
|
||||
import { useRouter } from 'vue-router'
|
||||
import Question from '@/components/Modals/Question.vue'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const showQuestionModal = ref(false)
|
||||
const currentQuestion = reactive({
|
||||
question: '',
|
||||
@@ -453,12 +456,10 @@ const breadcrumbs = computed(() => {
|
||||
return crumbs
|
||||
})
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: props.quizID == 'new' ? __('New Quiz') : quizDetails.data?.title,
|
||||
description: __('Form to create and edit quizzes'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
|
||||
@@ -14,11 +14,12 @@
|
||||
</template>
|
||||
<script setup>
|
||||
import Quiz from '@/components/Quiz.vue'
|
||||
import { createResource, Breadcrumbs } from 'frappe-ui'
|
||||
import { createResource, Breadcrumbs, usePageMeta } from 'frappe-ui'
|
||||
import { computed, inject, onMounted, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
import { sessionStore } from '../stores/session'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const user = inject('$user')
|
||||
const router = useRouter()
|
||||
const fromLesson = ref(false)
|
||||
@@ -56,12 +57,10 @@ const breadcrumbs = computed(() => {
|
||||
return [{ label: __('Quiz Submission') }, { label: title.data?.title }]
|
||||
})
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: title.data?.title,
|
||||
description: __('Quiz Submission'),
|
||||
title: `${title.data?.title}`,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
|
||||
@@ -79,11 +79,14 @@ import {
|
||||
FormControl,
|
||||
Button,
|
||||
Badge,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, onBeforeUnmount, onMounted, inject } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { showToast } from '@/utils'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const router = useRouter()
|
||||
const user = inject('$user')
|
||||
|
||||
@@ -149,4 +152,11 @@ const saveSubmission = () => {
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: `${submisisonDetails.doc.quiz_title}`,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -40,6 +40,18 @@
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
|
||||
>
|
||||
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
|
||||
<div class="text-xl font-medium">
|
||||
{{ __('No submissions') }}
|
||||
</div>
|
||||
<div class="leading-5">
|
||||
{{ __('No quiz submissions found. Please check again later.') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import {
|
||||
@@ -51,10 +63,14 @@ import {
|
||||
ListRows,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { BookOpen } from 'lucide-vue-next'
|
||||
import { computed, onMounted, inject } from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const router = useRouter()
|
||||
const user = inject('$user')
|
||||
|
||||
@@ -105,4 +121,11 @@ const quizColumns = computed(() => {
|
||||
const breadcrumbs = computed(() => {
|
||||
return [{ label: __('Quiz Submissions') }]
|
||||
})
|
||||
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: __('Quiz Submissions'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -79,12 +79,14 @@ import {
|
||||
ListRow,
|
||||
ListHeader,
|
||||
ListHeaderItem,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { computed, inject, onMounted } from 'vue'
|
||||
import { BookOpen, Plus } from 'lucide-vue-next'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
import { sessionStore } from '@/stores/session'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const user = inject('$user')
|
||||
const router = useRouter()
|
||||
|
||||
@@ -143,12 +145,10 @@ const breadcrumbs = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: __('Quizzes'),
|
||||
description: __('List of quizzes'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
|
||||
@@ -39,11 +39,13 @@ import {
|
||||
createDocumentResource,
|
||||
createListResource,
|
||||
createResource,
|
||||
usePageMeta,
|
||||
} from 'frappe-ui'
|
||||
import { computed, inject, onBeforeMount, ref } from 'vue'
|
||||
import { useSidebar } from '@/stores/sidebar'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
import { sessionStore } from '../stores/session'
|
||||
|
||||
const { brand } = sessionStore()
|
||||
const sidebarStore = useSidebar()
|
||||
const user = inject('$user')
|
||||
const readyToRender = ref(false)
|
||||
@@ -195,14 +197,10 @@ const breadcrumbs = computed(() => {
|
||||
]
|
||||
})
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: chapter?.doc?.title,
|
||||
description: __('This is a chapter in the course {0}').format(
|
||||
chapter?.doc?.course_title
|
||||
),
|
||||
title: chapter.doc?.title,
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
|
||||
@@ -117,9 +117,9 @@
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { createResource, Breadcrumbs } from 'frappe-ui'
|
||||
import { computed, inject } from 'vue'
|
||||
import { updateDocumentTitle } from '@/utils'
|
||||
import { createResource, Breadcrumbs, usePageMeta } from 'frappe-ui'
|
||||
import { computed } from 'vue'
|
||||
import { sessionStore } from '../stores/session'
|
||||
import { formatNumber } from '@/utils'
|
||||
import { Line, Pie } from 'vue-chartjs'
|
||||
import {
|
||||
@@ -154,7 +154,7 @@ import {
|
||||
BookOpenCheck,
|
||||
} from 'lucide-vue-next'
|
||||
|
||||
const dayjs = inject('$dayjs')
|
||||
const { brand } = sessionStore()
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
return [
|
||||
@@ -317,12 +317,10 @@ const chartOptions = (isPie) => {
|
||||
}
|
||||
}
|
||||
|
||||
const pageMeta = computed(() => {
|
||||
usePageMeta(() => {
|
||||
return {
|
||||
title: 'Statistics',
|
||||
description: 'Statistics of the platform',
|
||||
title: __('Statistics'),
|
||||
icon: brand.favicon,
|
||||
}
|
||||
})
|
||||
|
||||
updateDocumentTitle(pageMeta)
|
||||
</script>
|
||||
|
||||
@@ -2,10 +2,11 @@ import { defineStore } from 'pinia'
|
||||
import { createResource } from 'frappe-ui'
|
||||
import { usersStore } from './user'
|
||||
import router from '@/router'
|
||||
import { ref, computed } from 'vue'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
|
||||
export const sessionStore = defineStore('lms-session', () => {
|
||||
let { userResource } = usersStore()
|
||||
const brand = reactive({})
|
||||
|
||||
function sessionUser() {
|
||||
let cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
|
||||
@@ -46,7 +47,11 @@ export const sessionStore = defineStore('lms-session', () => {
|
||||
cache: 'brand',
|
||||
auto: true,
|
||||
onSuccess(data) {
|
||||
document.querySelector("link[rel='icon']").href = data.favicon
|
||||
brand.name = data.app_name
|
||||
brand.logo = data.app_logo
|
||||
brand.favicon =
|
||||
data.favicon?.file_url ||
|
||||
'/assets/lms/frontend/public/learning.svg'
|
||||
},
|
||||
})
|
||||
|
||||
@@ -61,6 +66,7 @@ export const sessionStore = defineStore('lms-session', () => {
|
||||
isLoggedIn,
|
||||
login,
|
||||
logout,
|
||||
brand,
|
||||
branding,
|
||||
sidebarSettings,
|
||||
}
|
||||
|
||||
@@ -281,6 +281,7 @@ def get_job_details(job):
|
||||
"type",
|
||||
"company_name",
|
||||
"company_logo",
|
||||
"company_website",
|
||||
"name",
|
||||
"creation",
|
||||
"description",
|
||||
|
||||
@@ -11,66 +11,34 @@ import json
|
||||
|
||||
|
||||
class CourseLesson(Document):
|
||||
def validate(self):
|
||||
# self.check_and_create_folder()
|
||||
def on_update(self):
|
||||
self.validate_quiz_id()
|
||||
|
||||
def validate_quiz_id(self):
|
||||
if self.quiz_id and not frappe.db.exists("LMS Quiz", self.quiz_id):
|
||||
frappe.throw(_("Invalid Quiz ID"))
|
||||
|
||||
def on_update(self):
|
||||
dynamic_documents = ["Exercise", "Quiz"]
|
||||
for section in dynamic_documents:
|
||||
self.update_lesson_name_in_document(section)
|
||||
if self.content:
|
||||
self.save_lesson_details_in_quiz(self.content)
|
||||
|
||||
def update_lesson_name_in_document(self, section):
|
||||
doctype_map = {"Exercise": "LMS Exercise", "Quiz": "LMS Quiz"}
|
||||
macros = find_macros(self.body)
|
||||
documents = [value for name, value in macros if name == section]
|
||||
index = 1
|
||||
for name in documents:
|
||||
e = frappe.get_doc(doctype_map[section], name)
|
||||
e.lesson = self.name
|
||||
e.index_ = index
|
||||
e.course = self.course
|
||||
e.save(ignore_permissions=True)
|
||||
index += 1
|
||||
self.update_orphan_documents(doctype_map[section], documents)
|
||||
if self.instructor_content:
|
||||
self.save_lesson_details_in_quiz(self.instructor_content)
|
||||
|
||||
def update_orphan_documents(self, doctype, documents):
|
||||
"""Updates the documents that were previously part of this lesson,
|
||||
but not any more.
|
||||
"""
|
||||
linked_documents = {
|
||||
row["name"] for row in frappe.get_all(doctype, {"lesson": self.name})
|
||||
}
|
||||
active_documents = set(documents)
|
||||
orphan_documents = linked_documents - active_documents
|
||||
for name in orphan_documents:
|
||||
ex = frappe.get_doc(doctype, name)
|
||||
ex.lesson = None
|
||||
ex.course = None
|
||||
ex.index_ = 0
|
||||
ex.save(ignore_permissions=True)
|
||||
|
||||
def check_and_create_folder(self):
|
||||
args = {
|
||||
"doctype": "File",
|
||||
"is_folder": True,
|
||||
"file_name": f"{self.name} {self.course}",
|
||||
}
|
||||
if not frappe.db.exists(args):
|
||||
folder = frappe.get_doc(args)
|
||||
folder.save(ignore_permissions=True)
|
||||
|
||||
def get_exercises(self):
|
||||
if not self.body:
|
||||
return []
|
||||
|
||||
macros = find_macros(self.body)
|
||||
exercises = [value for name, value in macros if name == "Exercise"]
|
||||
return [frappe.get_doc("LMS Exercise", name) for name in exercises]
|
||||
def save_lesson_details_in_quiz(self, content):
|
||||
content = json.loads(self.content)
|
||||
for block in content.get("blocks"):
|
||||
if block.get("type") == "quiz":
|
||||
quiz = block.get("data").get("quiz")
|
||||
if not frappe.db.exists("LMS Quiz", quiz):
|
||||
frappe.throw(_("Invalid Quiz ID in content"))
|
||||
frappe.db.set_value(
|
||||
"LMS Quiz",
|
||||
quiz,
|
||||
{
|
||||
"course": self.course,
|
||||
"lesson": self.name,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@@ -102,7 +70,7 @@ def save_progress(lesson, course):
|
||||
progress = get_course_progress(course)
|
||||
capture_progress_for_analytics(progress, course)
|
||||
|
||||
# Had to get doc, as on_change doesn't trigger when you use set_value. The trigger is necesary for badge to get assigned.
|
||||
# Had to get doc, as on_change doesn't trigger when you use set_value. The trigger is necessary for badge to get assigned.
|
||||
enrollment = frappe.get_doc("LMS Enrollment", membership)
|
||||
enrollment.progress = progress
|
||||
enrollment.save()
|
||||
|
||||
@@ -136,9 +136,15 @@
|
||||
"label": "Duration (in minutes)"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2025-01-06 11:02:09.749207",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "LMS Quiz Submission",
|
||||
"link_fieldname": "quiz"
|
||||
}
|
||||
],
|
||||
"modified": "2025-04-07 15:03:48.525458",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Quiz",
|
||||
@@ -190,10 +196,11 @@
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"show_title_field_in_link": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,6 @@ from fuzzywuzzy import fuzz
|
||||
from lms.lms.doctype.course_lesson.course_lesson import save_progress
|
||||
from lms.lms.utils import (
|
||||
generate_slug,
|
||||
has_course_moderator_role,
|
||||
has_course_instructor_role,
|
||||
)
|
||||
from binascii import Error as BinasciiError
|
||||
from frappe.utils.file_manager import safe_b64decode
|
||||
|
||||
@@ -27,8 +27,9 @@
|
||||
"signup_settings_tab",
|
||||
"signup_settings_section",
|
||||
"column_break_9",
|
||||
"custom_signup_content",
|
||||
"user_category",
|
||||
"disable_signup",
|
||||
"custom_signup_content",
|
||||
"sidebar_tab",
|
||||
"items_in_sidebar_section",
|
||||
"courses",
|
||||
@@ -104,7 +105,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "user_category",
|
||||
"fieldtype": "Check",
|
||||
"label": "Ask User Category during Signup"
|
||||
"label": "Identify User Persona"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -365,12 +366,19 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Payment Reminder Template",
|
||||
"options": "Email Template"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disable_signup",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Signup"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2025-02-11 11:29:43.412897",
|
||||
"modified": "2025-04-07 18:05:52.000651",
|
||||
"modified_by": "Administrator",
|
||||
"module": "LMS",
|
||||
"name": "LMS Settings",
|
||||
@@ -394,8 +402,9 @@
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"row_format": "Dynamic",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ from frappe.utils import get_url_to_list
|
||||
class LMSSettings(Document):
|
||||
def validate(self):
|
||||
self.validate_google_settings()
|
||||
self.validate_signup()
|
||||
|
||||
def validate_google_settings(self):
|
||||
if self.send_calendar_invite_for_evaluations:
|
||||
@@ -40,6 +41,10 @@ class LMSSettings(Document):
|
||||
)
|
||||
)
|
||||
|
||||
def validate_signup(self):
|
||||
if self.has_value_changed("disable_signup"):
|
||||
frappe.db.set_single_value("Website Settings", "disable_signup", self.disable_signup)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def check_payments_app():
|
||||
|
||||
383
lms/locale/ar.po
383
lms/locale/ar.po
File diff suppressed because it is too large
Load Diff
383
lms/locale/bs.po
383
lms/locale/bs.po
File diff suppressed because it is too large
Load Diff
385
lms/locale/de.po
385
lms/locale/de.po
File diff suppressed because it is too large
Load Diff
383
lms/locale/eo.po
383
lms/locale/eo.po
File diff suppressed because it is too large
Load Diff
383
lms/locale/es.po
383
lms/locale/es.po
File diff suppressed because it is too large
Load Diff
395
lms/locale/fa.po
395
lms/locale/fa.po
File diff suppressed because it is too large
Load Diff
383
lms/locale/fr.po
383
lms/locale/fr.po
File diff suppressed because it is too large
Load Diff
383
lms/locale/hr.po
383
lms/locale/hr.po
File diff suppressed because it is too large
Load Diff
383
lms/locale/hu.po
383
lms/locale/hu.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
383
lms/locale/pl.po
383
lms/locale/pl.po
File diff suppressed because it is too large
Load Diff
6235
lms/locale/pt_BR.po
Normal file
6235
lms/locale/pt_BR.po
Normal file
File diff suppressed because it is too large
Load Diff
383
lms/locale/ru.po
383
lms/locale/ru.po
File diff suppressed because it is too large
Load Diff
383
lms/locale/sv.po
383
lms/locale/sv.po
File diff suppressed because it is too large
Load Diff
383
lms/locale/th.po
383
lms/locale/th.po
File diff suppressed because it is too large
Load Diff
383
lms/locale/tr.po
383
lms/locale/tr.po
File diff suppressed because it is too large
Load Diff
2353
lms/locale/zh.po
2353
lms/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -17,7 +17,7 @@
|
||||
<label class="form-label sr-only"> {{ _("User Category") }} </label>
|
||||
<div class="control-input-wrapper">
|
||||
<div class="control-input flex align-center">
|
||||
<select type="text" id="user_category" data-fieldname="user_category" style="color: var(--text-muted)"
|
||||
<select type="text" id="user_category" data-fieldname="user_category" style="color: var(--text-light)"
|
||||
class="input-with-feedback form-control ellipsis" data-fieldtype="Select" required>
|
||||
<option value=""> {{ _("Category") }} </option>
|
||||
<option value="Business Owner"> {{ _("Business Owner") }} </option>
|
||||
|
||||
153
lms/www/lms.py
153
lms/www/lms.py
@@ -10,25 +10,58 @@ no_cache = 1
|
||||
|
||||
def get_context():
|
||||
app_path = frappe.form_dict.get("app_path")
|
||||
context = frappe._dict()
|
||||
if app_path:
|
||||
context.meta = get_meta(app_path)
|
||||
else:
|
||||
context.meta = {}
|
||||
favicon = (
|
||||
frappe.db.get_single_value("Website Settings", "favicon")
|
||||
or "/assets/lms/frontend/favicon.png"
|
||||
)
|
||||
title = frappe.db.get_single_value("Website Settings", "app_name") or "Frappe Learning"
|
||||
csrf_token = frappe.sessions.get_csrf_token()
|
||||
frappe.db.commit() # nosemgrep
|
||||
frappe.db.commit()
|
||||
|
||||
context = frappe._dict()
|
||||
context.csrf_token = csrf_token
|
||||
context.setup_complete = cint(frappe.get_system_settings("setup_complete"))
|
||||
context.meta = get_meta(app_path, title, favicon)
|
||||
capture("active_site", "lms")
|
||||
context.favicon = frappe.db.get_single_value("Website Settings", "favicon")
|
||||
context.title = title
|
||||
context.favicon = favicon
|
||||
return context
|
||||
|
||||
|
||||
def get_meta(app_path):
|
||||
def get_meta(app_path, title, favicon):
|
||||
meta = {}
|
||||
if app_path:
|
||||
meta = get_meta_from_document(app_path, favicon)
|
||||
|
||||
route_meta = frappe.get_all("Website Meta Tag", {"parent": app_path}, ["key", "value"])
|
||||
|
||||
if len(route_meta) > 0:
|
||||
for row in route_meta:
|
||||
if row.key == "title":
|
||||
meta["title"] = row.value
|
||||
elif row.key == "image":
|
||||
meta["image"] = row.value
|
||||
elif row.key == "description":
|
||||
meta["description"] = f"{meta.get('description', '')} {row.value}"
|
||||
elif row.key == "keywords":
|
||||
meta["keywords"] = f"{meta.get('keywords', '')} {row.value}"
|
||||
elif row.key == "link":
|
||||
meta["link"] = row.value
|
||||
|
||||
if not meta:
|
||||
meta = {
|
||||
"title": title,
|
||||
"image": favicon,
|
||||
"description": "Easy to use Learning Management System",
|
||||
}
|
||||
|
||||
return meta
|
||||
|
||||
|
||||
def get_meta_from_document(app_path, favicon):
|
||||
if app_path == "courses":
|
||||
return {
|
||||
"title": _("Course List"),
|
||||
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
|
||||
"image": favicon,
|
||||
"description": "This page lists all the courses published on our website",
|
||||
"keywords": "All Courses, Courses, Learn",
|
||||
"link": "/courses",
|
||||
@@ -47,13 +80,18 @@ def get_meta(app_path):
|
||||
course = frappe.db.get_value(
|
||||
"LMS Course",
|
||||
course_name,
|
||||
["title", "image", "short_introduction", "tags"],
|
||||
["title", "image", "description", "tags"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if course.description:
|
||||
soup = BeautifulSoup(course.description, "html.parser")
|
||||
course.description = soup.get_text()
|
||||
|
||||
return {
|
||||
"title": course.title,
|
||||
"image": course.image,
|
||||
"description": course.short_introduction,
|
||||
"description": course.description,
|
||||
"keywords": course.tags,
|
||||
"link": f"/courses/{course_name}",
|
||||
}
|
||||
@@ -61,7 +99,7 @@ def get_meta(app_path):
|
||||
if app_path == "batches":
|
||||
return {
|
||||
"title": _("Batches"),
|
||||
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
|
||||
"image": favicon,
|
||||
"description": "This page lists all the batches published on our website",
|
||||
"keywords": "All Batches, Batches, Learn",
|
||||
"link": "/batches",
|
||||
@@ -71,13 +109,18 @@ def get_meta(app_path):
|
||||
batch = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
batch_name,
|
||||
["title", "meta_image", "description", "category", "medium"],
|
||||
["title", "meta_image", "batch_details", "category", "medium"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if batch.batch_details:
|
||||
soup = BeautifulSoup(batch.batch_details, "html.parser")
|
||||
batch.batch_details = soup.get_text()
|
||||
|
||||
return {
|
||||
"title": batch.title,
|
||||
"image": batch.meta_image,
|
||||
"description": batch.description,
|
||||
"description": batch.batch_details,
|
||||
"keywords": f"{batch.category} {batch.medium}",
|
||||
"link": f"/batches/details/{batch_name}",
|
||||
}
|
||||
@@ -87,7 +130,7 @@ def get_meta(app_path):
|
||||
if "new/edit" in app_path:
|
||||
return {
|
||||
"title": _("New Batch"),
|
||||
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
|
||||
"image": favicon,
|
||||
"description": "Create a new batch",
|
||||
"keywords": "New Batch, Create Batch",
|
||||
"link": "/lms/batches/new/edit",
|
||||
@@ -95,13 +138,18 @@ def get_meta(app_path):
|
||||
batch = frappe.db.get_value(
|
||||
"LMS Batch",
|
||||
batch_name,
|
||||
["title", "meta_image", "description", "category", "medium"],
|
||||
["title", "meta_image", "batch_details", "category", "medium"],
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
if batch.batch_details:
|
||||
soup = BeautifulSoup(batch.batch_details, "html.parser")
|
||||
batch.batch_details = soup.get_text()
|
||||
|
||||
return {
|
||||
"title": batch.title,
|
||||
"image": batch.meta_image,
|
||||
"description": batch.description,
|
||||
"description": batch.batch_details,
|
||||
"keywords": f"{batch.category} {batch.medium}",
|
||||
"link": f"/batches/{batch_name}",
|
||||
}
|
||||
@@ -109,7 +157,7 @@ def get_meta(app_path):
|
||||
if app_path == "job-openings":
|
||||
return {
|
||||
"title": _("Job Openings"),
|
||||
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
|
||||
"image": favicon,
|
||||
"description": "This page lists all the job openings published on our website",
|
||||
"keywords": "Job Openings, Jobs, Vacancies",
|
||||
"link": "/job-openings",
|
||||
@@ -120,13 +168,13 @@ def get_meta(app_path):
|
||||
job_opening = frappe.db.get_value(
|
||||
"Job Opportunity",
|
||||
job_opening_name,
|
||||
["job_title", "company_logo", "company_name"],
|
||||
["job_title", "company_logo", "description"],
|
||||
as_dict=True,
|
||||
)
|
||||
return {
|
||||
"title": job_opening.job_title,
|
||||
"image": job_opening.company_logo,
|
||||
"description": job_opening.company_name,
|
||||
"description": job_opening.description,
|
||||
"keywords": "Job Openings, Jobs, Vacancies",
|
||||
"link": f"/job-openings/{job_opening_name}",
|
||||
}
|
||||
@@ -134,7 +182,7 @@ def get_meta(app_path):
|
||||
if app_path == "statistics":
|
||||
return {
|
||||
"title": _("Statistics"),
|
||||
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
|
||||
"image": favicon,
|
||||
"description": "This page lists all the statistics of this platform",
|
||||
"keywords": "Enrollment Count, Completion, Signups",
|
||||
"link": "/statistics",
|
||||
@@ -179,3 +227,64 @@ def get_meta(app_path):
|
||||
"keywords": f"{badge.title}, {badge.description}",
|
||||
"link": f"/badges/{badgeName}/{email}",
|
||||
}
|
||||
|
||||
if app_path == "quizzes":
|
||||
return {
|
||||
"title": _("Quizzes"),
|
||||
"image": favicon,
|
||||
"description": _("Test your knowledge with interactive quizzes and more."),
|
||||
"keywords": "Quizzes, interactive quizzes, online quizzes",
|
||||
"link": "/quizzes",
|
||||
}
|
||||
|
||||
if re.match(r"^quizzes/[^/]+$", app_path):
|
||||
quiz_name = app_path.split("/")[1]
|
||||
quiz = frappe.db.get_value(
|
||||
"LMS Quiz",
|
||||
quiz_name,
|
||||
["title"],
|
||||
as_dict=True,
|
||||
)
|
||||
if quiz:
|
||||
return {
|
||||
"title": quiz.title,
|
||||
"image": favicon,
|
||||
"description": "Test your knowledge with interactive quizzes.",
|
||||
"keywords": quiz.title,
|
||||
"link": f"/quizzes/{quiz_name}",
|
||||
}
|
||||
|
||||
if app_path == "assignments":
|
||||
return {
|
||||
"title": _("Assignments"),
|
||||
"image": favicon,
|
||||
"description": _("Test your knowledge with interactive assignments and more."),
|
||||
"keywords": "Assignments, interactive assignments, online assignments",
|
||||
"link": "/assignments",
|
||||
}
|
||||
|
||||
if re.match(r"^assignments/[^/]+$", app_path):
|
||||
assignment_name = app_path.split("/")[1]
|
||||
assignment = frappe.db.get_value(
|
||||
"LMS Assignment",
|
||||
assignment_name,
|
||||
["title"],
|
||||
as_dict=True,
|
||||
)
|
||||
if assignment:
|
||||
return {
|
||||
"title": assignment.title,
|
||||
"image": favicon,
|
||||
"description": "Test your knowledge with interactive assignments.",
|
||||
"keywords": assignment.title,
|
||||
"link": f"/assignments/{assignment_name}",
|
||||
}
|
||||
|
||||
if app_path == "programs":
|
||||
return {
|
||||
"title": _("Programs"),
|
||||
"image": favicon,
|
||||
"description": "This page lists all the programs published on our website",
|
||||
"keywords": "All Programs, Programs, Learn",
|
||||
"link": "/programs",
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user