Merge pull request #1417 from frappe/develop

chore: merge 'develop' into 'main'
This commit is contained in:
Jannat Patel
2025-04-09 15:17:25 +05:30
committed by GitHub
72 changed files with 11713 additions and 3620 deletions

View File

@@ -7,8 +7,27 @@ on:
branches: [ main ] branches: [ main ]
jobs: 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: linters:
name: Semantic Commits name: Semgrep Rules
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.event_name == 'pull_request' if: github.event_name == 'pull_request'
@@ -20,8 +39,17 @@ jobs:
with: with:
python-version: '3.10' 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 - name: Install and Run Pre-commit
uses: pre-commit/action@v2.0.3 uses: pre-commit/action@v3.0.1
- name: Download Semgrep rules - name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules

View File

@@ -70,7 +70,7 @@ jobs:
id: yarn-cache-dir-path id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- uses: actions/cache@v3 - uses: actions/cache@v4
id: yarn-cache id: yarn-cache
with: with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }} path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -79,7 +79,7 @@ jobs:
${{ runner.os }}-yarn-ui- ${{ runner.os }}-yarn-ui-
- name: Cache cypress binary - name: Cache cypress binary
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: ~/.cache/Cypress path: ~/.cache/Cypress
key: ${{ runner.os }}-cypress key: ${{ runner.os }}-cypress

26
commitlint.config.js Normal file
View 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
],
],
},
};

View File

@@ -2,9 +2,9 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <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" /> <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="title" content="{{ meta.title }}" />
<meta name="image" content="{{ meta.image }}" /> <meta name="image" content="{{ meta.image }}" />
<meta name="description" content="{{ meta.description }}" /> <meta name="description" content="{{ meta.description }}" />
@@ -23,17 +23,6 @@
<p> <p>
{{ meta.description }} {{ meta.description }}
</p> </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> <a href="{{ meta.link }}">Know More</a>
</div> </div>
</div> </div>
@@ -41,9 +30,8 @@
<div id="popovers"></div> <div id="popovers"></div>
<script> <script>
window.csrf_token = '{{ csrf_token }}'
window.setup_complete = '{{ setup_complete }}'
document.getElementById('seo-content').style.display = 'none'; document.getElementById('seo-content').style.display = 'none';
window.csrf_token = '{{ csrf_token }}'
</script> </script>
<script type="module" src="/src/main.js"></script> <script type="module" src="/src/main.js"></script>
</body> </body>

View 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

View File

@@ -146,7 +146,6 @@ function resetEditor(value: string, resetHistory = false) {
value = getModelValue() value = getModelValue()
aceEditor?.setValue(value) aceEditor?.setValue(value)
aceEditor?.clearSelection() aceEditor?.clearSelection()
console.log(isDark.value)
aceEditor?.setTheme(isDark.value ? 'ace/theme/twilight' : 'ace/theme/chrome') aceEditor?.setTheme(isDark.value ? 'ace/theme/twilight' : 'ace/theme/chrome')
props.autofocus && aceEditor?.focus() props.autofocus && aceEditor?.focus()
if (resetHistory) { if (resetHistory) {

View File

@@ -142,6 +142,7 @@
</div> </div>
</div> </div>
<ChapterModal <ChapterModal
v-if="user.data"
v-model="showChapterModal" v-model="showChapterModal"
v-model:outline="outline" v-model:outline="outline"
:course="courseName" :course="courseName"

View File

@@ -1,36 +1,18 @@
<template> <template>
<svg <svg
width="118" width="80"
height="118" height="79"
viewBox="0 0 118 118" viewBox="0 0 80 79"
fill="none" fill="none"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
> >
<path <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" 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="url(#paint0_radial_174_336)" fill="#0E7159"
/> />
<path <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" 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="#0B3D3D" fill="white"
fill-opacity="0.8"
/> />
<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> </svg>
</template> </template>

View File

@@ -1,41 +1,37 @@
<template> <template>
<div class="flex space-x-4 border rounded-md p-2"> <div class="border rounded-md p-4">
<img :src="job.company_logo" class="size-10 rounded-full object-contain" /> <div class="flex space-x-4">
<div class="flex flex-col space-y-2 flex-1"> <img
<div class="flex items-center justify-between"> :src="job.company_logo"
<span class="font-semibold text-ink-gray-9"> class="size-10 rounded-full object-contain"
{{ job.job_title }} />
</span> <div class="flex flex-col space-y-1 flex-1">
</div> <div class="flex items-center justify-between">
<div class="flex items-center space-x-2 text-ink-gray-5"> <span class="text-lg font-semibold text-ink-gray-9">
<Building2 class="w-4 h-4 stroke-1.5" /> {{ job.job_title }}
<span> </span>
</div>
<div class="text-xs text-ink-gray-5">
{{ job.company_name }} {{ job.company_name }}
</span> </div>
</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>
<div class="space-x-4 mt-2">
<Badge>
{{ job.location }}
</Badge>
<Badge>
{{ job.type }}
</Badge>
<Badge>
{{ dayjs(job.creation).fromNow() }}
</Badge>
</div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { Building2, Calendar, MapPin, Shapes } from 'lucide-vue-next'
import { inject } from 'vue' import { inject } from 'vue'
import { Avatar } from 'frappe-ui' import { Badge } from 'frappe-ui'
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
const props = defineProps({ const props = defineProps({

View File

@@ -119,7 +119,7 @@ const { updateOnboardingStep } = useOnboarding('learning')
const existingQuestion = reactive({ const existingQuestion = reactive({
question: '', question: '',
marks: 0, marks: 1,
}) })
const question = reactive({ const question = reactive({
question: '', question: '',

View File

@@ -328,19 +328,26 @@ const tabsStructure = computed(() => {
icon: 'LogIn', icon: 'LogIn',
fields: [ 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', name: 'custom_signup_content',
type: 'Code', type: 'Code',
mode: 'htmlmixed', mode: 'htmlmixed',
rows: 10, 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.',
},
], ],
}, },
], ],

View File

@@ -1,7 +1,7 @@
<template> <template>
<div v-if="quiz.data"> <div v-if="quiz.data">
<div <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"> <div class="leading-5">
{{ {{

View File

@@ -99,7 +99,7 @@
</template> </template>
<script setup> <script setup>
import { FormControl, FileUploader, Button, Switch } from 'frappe-ui' import { FormControl, FileUploader, Button, Switch } from 'frappe-ui'
import { computed } from 'vue' import { computed, onMounted } from 'vue'
import { getFileSize, validateFile } from '@/utils' import { getFileSize, validateFile } from '@/utils'
import { X } from 'lucide-vue-next' import { X } from 'lucide-vue-next'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'

View File

@@ -36,7 +36,7 @@
<span v-else> Learning </span> <span v-else> Learning </span>
</div> </div>
<div <div
v-if="userResource" v-if="userResource.data"
class="mt-1 text-sm text-ink-gray-7 leading-none" class="mt-1 text-sm text-ink-gray-7 leading-none"
> >
{{ convertToTitleCase(userResource.data?.full_name) }} {{ convertToTitleCase(userResource.data?.full_name) }}

View File

@@ -63,6 +63,7 @@ import {
createResource, createResource,
FormControl, FormControl,
TextEditor, TextEditor,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
computed, computed,
@@ -72,11 +73,13 @@ import {
reactive, reactive,
watch, watch,
} from 'vue' } from 'vue'
import { sessionStore } from '../stores/session'
import { showToast } from '@/utils' import { showToast } from '@/utils'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const user = inject('$user') const user = inject('$user')
const router = useRouter() const router = useRouter()
const { brand } = sessionStore()
const props = defineProps({ const props = defineProps({
assignmentID: { assignmentID: {
@@ -188,4 +191,11 @@ const assignmentOptions = computed(() => {
{ label: 'URL', value: 'URL' }, { label: 'URL', value: 'URL' },
] ]
}) })
usePageMeta(() => {
return {
title: assignment.doc ? assignment.doc.title : __('New Assignment'),
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -14,12 +14,14 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { Breadcrumbs, createResource } from 'frappe-ui' import { Breadcrumbs, createResource, usePageMeta } from 'frappe-ui'
import { computed, inject, onMounted, onBeforeUnmount, ref } from 'vue' import { computed, inject, onMounted, ref } from 'vue'
import { sessionStore } from '../stores/session'
import Assignment from '@/components/Assignment.vue' import Assignment from '@/components/Assignment.vue'
const user = inject('$user') const user = inject('$user')
const fromLesson = ref(false) const fromLesson = ref(false)
const { brand } = sessionStore()
const props = defineProps({ const props = defineProps({
assignmentID: { assignmentID: {
@@ -72,4 +74,11 @@ const breadcrumbs = computed(() => {
] ]
return crumbs return crumbs
}) })
usePageMeta(() => {
return {
title: title.data?.title,
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -84,14 +84,17 @@ import {
ListRows, ListRows,
ListRow, ListRow,
ListRowItem, ListRowItem,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue' import { computed, inject, onMounted, ref, watch } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { Pencil } from 'lucide-vue-next' import { Pencil } from 'lucide-vue-next'
import { sessionStore } from '../stores/session'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
const user = inject('$user') const user = inject('$user')
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
const { brand } = sessionStore()
const router = useRouter() const router = useRouter()
const assignmentID = ref('') const assignmentID = ref('')
const member = ref('') const member = ref('')
@@ -214,4 +217,11 @@ const breadcrumbs = computed(() => {
}, },
] ]
}) })
usePageMeta(() => {
return {
title: __('Assignment Submissions'),
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -80,15 +80,18 @@ import {
createListResource, createListResource,
FormControl, FormControl,
ListView, ListView,
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, Pencil } from 'lucide-vue-next' import { Plus, Pencil } from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session'
const user = inject('$user') const user = inject('$user')
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
const titleFilter = ref('') const titleFilter = ref('')
const typeFilter = ref('') const typeFilter = ref('')
const { brand } = sessionStore()
const router = useRouter() const router = useRouter()
onMounted(() => { onMounted(() => {
@@ -184,4 +187,11 @@ const breadcrumbs = computed(() => [
route: { name: 'Assignments' }, route: { name: 'Assignments' },
}, },
]) ])
usePageMeta(() => {
return {
title: __('Assignments'),
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -24,10 +24,12 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { createDocumentResource, createResource } from 'frappe-ui' import { createResource, usePageMeta } from 'frappe-ui'
import { computed, inject } from 'vue' import { computed, inject } from 'vue'
import { sessionStore } from '../stores/session'
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
const { brand } = sessionStore()
const props = defineProps({ const props = defineProps({
badgeName: { badgeName: {
@@ -70,4 +72,11 @@ const breadcrumbs = computed(() => {
}, },
] ]
}) })
usePageMeta(() => {
return {
title: badge.data.badge,
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -199,9 +199,14 @@
<script setup> <script setup>
import { computed, inject, ref, onMounted, watch } from 'vue' import { computed, inject, ref, onMounted, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { Breadcrumbs, Button, createResource, Tabs, Badge } from 'frappe-ui' import {
import CourseInstructors from '@/components/CourseInstructors.vue' Breadcrumbs,
import UserAvatar from '@/components/UserAvatar.vue' Button,
createResource,
Tabs,
Badge,
usePageMeta,
} from 'frappe-ui'
import { import {
Clock, Clock,
LayoutDashboard, LayoutDashboard,
@@ -214,7 +219,10 @@ import {
Globe, Globe,
ClipboardPen, ClipboardPen,
} from 'lucide-vue-next' } 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 BatchDashboard from '@/components/BatchDashboard.vue'
import BatchCourses from '@/components/BatchCourses.vue' import BatchCourses from '@/components/BatchCourses.vue'
import LiveClass from '@/components/LiveClass.vue' import LiveClass from '@/components/LiveClass.vue'
@@ -232,6 +240,7 @@ const showAnnouncementModal = ref(false)
const openCertificateDialog = ref(false) const openCertificateDialog = ref(false)
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const { brand } = sessionStore()
const tabIndex = ref(0) const tabIndex = ref(0)
const tabs = computed(() => { const tabs = computed(() => {
@@ -345,12 +354,10 @@ watch(tabIndex, () => {
} }
}) })
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: batch.data?.title, title: batch?.data?.title,
description: batch.data?.description, icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>

View File

@@ -102,8 +102,9 @@
import { computed, inject } from 'vue' import { computed, inject } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { BookOpen, Clock } from 'lucide-vue-next' import { BookOpen, Clock } from 'lucide-vue-next'
import { formatTime, updateDocumentTitle } from '@/utils' import { formatTime } from '@/utils'
import { Breadcrumbs, createResource } from 'frappe-ui' import { Breadcrumbs, createResource, usePageMeta } from 'frappe-ui'
import { sessionStore } from '@/stores/session'
import CourseCard from '@/components/CourseCard.vue' import CourseCard from '@/components/CourseCard.vue'
import BatchOverlay from '@/components/BatchOverlay.vue' import BatchOverlay from '@/components/BatchOverlay.vue'
import DateRange from '../components/Common/DateRange.vue' import DateRange from '../components/Common/DateRange.vue'
@@ -112,6 +113,7 @@ import UserAvatar from '@/components/UserAvatar.vue'
const user = inject('$user') const user = inject('$user')
const router = useRouter() const router = useRouter()
const { brand } = sessionStore()
const props = defineProps({ const props = defineProps({
batchName: { batchName: {
@@ -152,14 +154,12 @@ const breadcrumbs = computed(() => {
return items return items
}) })
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: batch.data?.title, title: batch?.data?.title,
description: batch.data?.description, icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>
<style> <style>
.batch-description p { .batch-description p {

View File

@@ -264,17 +264,20 @@ import {
Button, Button,
TextEditor, TextEditor,
createResource, createResource,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import Link from '@/components/Controls/Link.vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast } from '@/utils' import { showToast } from '@/utils'
import { Image } from 'lucide-vue-next' import { Image } from 'lucide-vue-next'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import { useOnboarding } from 'frappe-ui/frappe' 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 router = useRouter()
const user = inject('$user') const user = inject('$user')
const { brand } = sessionStore()
const { updateOnboardingStep } = useOnboarding('learning') const { updateOnboardingStep } = useOnboarding('learning')
const props = defineProps({ const props = defineProps({
@@ -505,4 +508,11 @@ const breadcrumbs = computed(() => {
}) })
return crumbs return crumbs
}) })
usePageMeta(() => {
return {
title: props.batchName == 'new' ? 'New Batch' : batchDetail.data?.title,
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -104,14 +104,16 @@ import {
FormControl, FormControl,
Select, Select,
TabButtons, TabButtons,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue' import { computed, inject, onMounted, ref, watch } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next' import { BookOpen, Plus } from 'lucide-vue-next'
import { updateDocumentTitle } from '@/utils' import { sessionStore } from '@/stores/session'
import BatchCard from '@/components/BatchCard.vue' import BatchCard from '@/components/BatchCard.vue'
const user = inject('$user') const user = inject('$user')
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
const { brand } = sessionStore()
const start = ref(0) const start = ref(0)
const pageLength = ref(20) const pageLength = ref(20)
const categories = ref([]) const categories = ref([])
@@ -304,12 +306,10 @@ const breadcrumbs = computed(() => [
}, },
]) ])
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: 'Batches', title: __('Batches'),
description: 'All upcoming batches.', icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>

View File

@@ -151,19 +151,20 @@
</template> </template>
<script setup> <script setup>
import { import {
Input,
Button, Button,
createResource, createResource,
FormControl, FormControl,
Breadcrumbs, Breadcrumbs,
Tooltip, usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { reactive, inject, onMounted, computed } from 'vue' import { reactive, inject, onMounted, computed } from 'vue'
import { showToast } from '@/utils/'
import { sessionStore } from '../stores/session'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import NotPermitted from '@/components/NotPermitted.vue' import NotPermitted from '@/components/NotPermitted.vue'
import { showToast } from '@/utils/'
const user = inject('$user') const user = inject('$user')
const { brand } = sessionStore()
onMounted(() => { onMounted(() => {
const script = document.createElement('script') const script = document.createElement('script')
@@ -356,4 +357,11 @@ const redirectTo = computed(() => {
return `/lms/courses/${props.name}/certification` return `/lms/courses/${props.name}/certification`
} }
}) })
usePageMeta(() => {
return {
title: __('Billing Details'),
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -102,14 +102,17 @@ import {
createListResource, createListResource,
FormControl, FormControl,
Select, Select,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, onMounted, ref } from 'vue' import { computed, onMounted, ref } from 'vue'
import { updateDocumentTitle } from '@/utils' import { updateDocumentTitle } from '@/utils'
import { BookOpen, GraduationCap } from 'lucide-vue-next' import { BookOpen, GraduationCap } from 'lucide-vue-next'
import { sessionStore } from '../stores/session'
const currentCategory = ref('') const currentCategory = ref('')
const filters = ref({}) const filters = ref({})
const nameFilter = ref('') const nameFilter = ref('')
const { brand } = sessionStore()
onMounted(() => { onMounted(() => {
updateParticipants() updateParticipants()
@@ -163,13 +166,12 @@ const breadcrumbs = computed(() => [
}, },
]) ])
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: 'Certified Participants', title: __('Certified Participants'),
description: 'All participants that have been certified.', icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>
<style> <style>
.headline { .headline {

View File

@@ -36,12 +36,14 @@
</template> </template>
<script setup> <script setup>
import { computed, inject, onMounted, ref } from 'vue' 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 { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session'
import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue' import UpcomingEvaluations from '@/components/UpcomingEvaluations.vue'
const courseTitle = ref(null) const courseTitle = ref(null)
const evaluator = ref(null) const evaluator = ref(null)
const { brand } = sessionStore()
const courses = ref([]) const courses = ref([])
const user = inject('$user') const user = inject('$user')
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
@@ -133,4 +135,11 @@ const breadcrumbs = computed(() => [
label: __('Certification'), label: __('Certification'),
}, },
]) ])
usePageMeta(() => {
return {
title: courseTitle.value,
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -92,16 +92,24 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { createResource, Breadcrumbs, Badge, Tooltip } from 'frappe-ui' import {
createResource,
Breadcrumbs,
Badge,
Tooltip,
usePageMeta,
} from 'frappe-ui'
import { computed } from 'vue' import { computed } from 'vue'
import { Users, Star } from 'lucide-vue-next' import { Users, Star } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import CourseCardOverlay from '@/components/CourseCardOverlay.vue' import CourseCardOverlay from '@/components/CourseCardOverlay.vue'
import CourseOutline from '@/components/CourseOutline.vue' import CourseOutline from '@/components/CourseOutline.vue'
import CourseReviews from '@/components/CourseReviews.vue' import CourseReviews from '@/components/CourseReviews.vue'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
import { updateDocumentTitle } from '@/utils'
import CourseInstructors from '@/components/CourseInstructors.vue' import CourseInstructors from '@/components/CourseInstructors.vue'
const { brand } = sessionStore()
const props = defineProps({ const props = defineProps({
courseName: { courseName: {
type: String, type: String,
@@ -127,14 +135,12 @@ const breadcrumbs = computed(() => {
return items return items
}) })
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: course?.data?.title, title: course?.data?.title,
description: course?.data?.short_introduction, icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>
<style> <style>
.avatar-group { .avatar-group {

View File

@@ -253,6 +253,7 @@ import {
createResource, createResource,
FormControl, FormControl,
FileUploader, FileUploader,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
inject, inject,
@@ -265,17 +266,19 @@ import {
getCurrentInstance, getCurrentInstance,
} from 'vue' } from 'vue'
import { showToast, updateDocumentTitle } from '@/utils' import { showToast, updateDocumentTitle } from '@/utils'
import Link from '@/components/Controls/Link.vue'
import { Image, Trash2, X } from 'lucide-vue-next' import { Image, Trash2, X } from 'lucide-vue-next'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
import { sessionStore } from '../stores/session'
import { useSettings } from '@/stores/settings' 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 user = inject('$user')
const newTag = ref('') const newTag = ref('')
const { brand } = sessionStore()
const router = useRouter() const router = useRouter()
const instructors = ref([]) const instructors = ref([])
const settingsStore = useSettings() const settingsStore = useSettings()
@@ -574,12 +577,10 @@ const breadcrumbs = computed(() => {
return crumbs return crumbs
}) })
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: 'Create a Course', title: courseResource.data?.title || __('New Course'),
description: 'Create or edit a course for your learning system.', icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>

View File

@@ -100,10 +100,11 @@ import {
FormControl, FormControl,
Select, Select,
TabButtons, TabButtons,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue' import { computed, inject, onMounted, ref, watch } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next' import { BookOpen, Plus } from 'lucide-vue-next'
import { updateDocumentTitle } from '@/utils' import { sessionStore } from '@/stores/session'
import CourseCard from '@/components/CourseCard.vue' import CourseCard from '@/components/CourseCard.vue'
const user = inject('$user') const user = inject('$user')
@@ -116,6 +117,7 @@ const title = ref('')
const certification = ref(false) const certification = ref(false)
const filters = ref({}) const filters = ref({})
const currentTab = ref('Live') const currentTab = ref('Live')
const { brand } = sessionStore()
onMounted(() => { onMounted(() => {
setFiltersFromQuery() setFiltersFromQuery()
@@ -303,12 +305,10 @@ const breadcrumbs = computed(() => [
}, },
]) ])
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: 'Courses', title: __('Courses'),
description: 'All published courses.', icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>

View File

@@ -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>

View File

@@ -139,14 +139,17 @@ import {
Button, Button,
TextEditor, TextEditor,
FileUploader, FileUploader,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, onMounted, reactive, inject } from 'vue' import { computed, onMounted, reactive, inject } from 'vue'
import { FileText, X } from 'lucide-vue-next' import { FileText, X } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { getFileSize, showToast } from '../utils' import { getFileSize, showToast } from '../utils'
const user = inject('$user') const user = inject('$user')
const router = useRouter() const router = useRouter()
const { brand } = sessionStore()
const props = defineProps({ const props = defineProps({
jobName: { jobName: {
@@ -319,4 +322,11 @@ const breadcrumbs = computed(() => {
] ]
return crumbs return crumbs
}) })
usePageMeta(() => {
return {
title: props.jobName == 'new' ? 'New Job' : jobDetail.data?.title,
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -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 <router-link
v-if="user.data.name == job.data?.owner" v-if="user.data.name == job.data?.owner"
:to="{ :to="{
@@ -24,13 +24,19 @@
params: { jobName: job.data?.name }, params: { jobName: job.data?.name },
}" }"
> >
<Button class="mr-2"> <Button>
<template #prefix> <template #prefix>
<Pencil class="h-4 w-4 stroke-1.5" /> <Pencil class="h-4 w-4 stroke-1.5" />
</template> </template>
{{ __('Edit') }} {{ __('Edit') }}
</Button> </Button>
</router-link> </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 <Button
v-if="!jobApplication.data?.length" v-if="!jobApplication.data?.length"
variant="solid" variant="solid"
@@ -56,10 +62,11 @@
<div class="flex items-center"> <div class="flex items-center">
<img <img
:src="job.data.company_logo" :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" :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 }} {{ job.data.job_title }}
</div> </div>
</div> </div>
@@ -69,7 +76,7 @@
> >
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<Building2 class="h-4 w-4 text-ink-green-2" /> <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"> <span class="text-xs text-ink-gray-5 font-medium uppercase">
{{ __('Organisation') }} {{ __('Organisation') }}
</span> </span>
@@ -80,7 +87,7 @@
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<MapPin class="size-4 text-ink-red-3" /> <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"> <span class="text-xs font-medium uppercase">
{{ __('Location') }} {{ __('Location') }}
</span> </span>
@@ -91,7 +98,7 @@
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<ClipboardType class="h-4 w-4 text-yellow-500" /> <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"> <span class="text-xs font-medium uppercase">
{{ __('Category') }} {{ __('Category') }}
</span> </span>
@@ -102,7 +109,7 @@
</div> </div>
<div class="flex items-center space-x-4"> <div class="flex items-center space-x-4">
<CalendarDays class="h-4 w-4 text-ink-blue-2" /> <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"> <span class="text-xs font-medium uppercase">
{{ __('Posted on') }} {{ __('Posted on') }}
</span> </span>
@@ -116,7 +123,7 @@
class="flex items-center space-x-4" class="flex items-center space-x-4"
> >
<SquareUserRound class="h-4 w-4 text-purple-500" /> <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"> <span class="text-xs font-medium uppercase">
{{ __('Applications Received') }} {{ __('Applications Received') }}
</span> </span>
@@ -142,9 +149,9 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { Button, Breadcrumbs, createResource } from 'frappe-ui' import { Button, Breadcrumbs, createResource, usePageMeta } from 'frappe-ui'
import { inject, ref, computed } from 'vue' import { inject, ref } from 'vue'
import { updateDocumentTitle } from '@/utils' import { sessionStore } from '../stores/session'
import JobApplicationModal from '@/components/Modals/JobApplicationModal.vue' import JobApplicationModal from '@/components/Modals/JobApplicationModal.vue'
import { import {
MapPin, MapPin,
@@ -154,10 +161,12 @@ import {
CalendarDays, CalendarDays,
ClipboardType, ClipboardType,
SquareUserRound, SquareUserRound,
SquareArrowOutUpRight,
} from 'lucide-vue-next' } from 'lucide-vue-next'
const user = inject('$user') const user = inject('$user')
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
const { brand } = sessionStore()
const showApplicationModal = ref(false) const showApplicationModal = ref(false)
const props = defineProps({ const props = defineProps({
@@ -215,12 +224,14 @@ const redirectToLogin = (job) => {
window.location.href = `/login?redirect-to=/job-openings/${job}` window.location.href = `/login?redirect-to=/job-openings/${job}`
} }
const pageMeta = computed(() => { const redirectToWebsite = (url) => {
window.open(url, '_blank')
}
usePageMeta(() => {
return { return {
title: job.data?.job_title, title: job.data?.job_title,
description: job.data?.description, icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>

View File

@@ -25,7 +25,7 @@
</router-link> </router-link>
</header> </header>
<div> <div>
<div class="lg:w-3/4 mx-auto p-5"> <div class="p-5">
<div <div
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5" 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 <div
v-if="jobs.data?.length" 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 <router-link
v-for="job in jobs.data" v-for="job in jobs.data"
@@ -81,14 +81,21 @@
</div> </div>
</template> </template>
<script setup> <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 { Plus, Search } from 'lucide-vue-next'
import { sessionStore } from '../stores/session'
import { inject, computed, ref, onMounted } from 'vue' import { inject, computed, ref, onMounted } from 'vue'
import JobCard from '@/components/JobCard.vue' import JobCard from '@/components/JobCard.vue'
import { updateDocumentTitle } from '@/utils'
const user = inject('$user') const user = inject('$user')
const jobType = ref(null) const jobType = ref(null)
const { brand } = sessionStore()
const searchQuery = ref('') const searchQuery = ref('')
const filters = ref({}) const filters = ref({})
const orFilters = ref({}) const orFilters = ref({})
@@ -147,12 +154,11 @@ const jobTypes = computed(() => {
{ label: __('Freelance'), value: 'Freelance' }, { label: __('Freelance'), value: 'Freelance' },
] ]
}) })
const pageMeta = computed(() => {
usePageMeta(() => {
return { return {
title: 'Jobs', title: __('Jobs'),
description: 'An open job board for the community', icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>

View File

@@ -193,14 +193,15 @@
</div> </div>
</template> </template>
<script setup> <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 { computed, watch, inject, ref, onMounted, onBeforeUnmount } from 'vue'
import CourseOutline from '@/components/CourseOutline.vue' import CourseOutline from '@/components/CourseOutline.vue'
import UserAvatar from '@/components/UserAvatar.vue' import UserAvatar from '@/components/UserAvatar.vue'
import { useRouter, useRoute } from 'vue-router' import { useRouter, useRoute } from 'vue-router'
import { ChevronLeft, ChevronRight, GraduationCap } from 'lucide-vue-next' import { ChevronLeft, ChevronRight, GraduationCap } from 'lucide-vue-next'
import Discussions from '@/components/Discussions.vue' 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 EditorJS from '@editorjs/editorjs'
import LessonContent from '@/components/LessonContent.vue' import LessonContent from '@/components/LessonContent.vue'
import CourseInstructors from '@/components/CourseInstructors.vue' import CourseInstructors from '@/components/CourseInstructors.vue'
@@ -215,6 +216,7 @@ const editor = ref(null)
const instructorEditor = ref(null) const instructorEditor = ref(null)
const lessonProgress = ref(0) const lessonProgress = ref(0)
const timer = ref(0) const timer = ref(0)
const { brand } = sessionStore()
let timerInterval let timerInterval
const props = defineProps({ const props = defineProps({
@@ -419,14 +421,12 @@ const redirectToLogin = () => {
window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}` window.location.href = `/login?redirect-to=/lms/courses/${props.courseName}`
} }
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: lesson.data?.title, title: lesson?.data?.title,
description: lesson.data?.course, icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>
<style> <style>
.avatar-group { .avatar-group {

View File

@@ -78,7 +78,13 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { Breadcrumbs, Button, createResource, FormControl } from 'frappe-ui' import {
Breadcrumbs,
Button,
createResource,
FormControl,
usePageMeta,
} from 'frappe-ui'
import { import {
computed, computed,
reactive, reactive,
@@ -87,13 +93,15 @@ import {
ref, ref,
onBeforeUnmount, onBeforeUnmount,
} from 'vue' } from 'vue'
import { sessionStore } from '../stores/session'
import EditorJS from '@editorjs/editorjs' import EditorJS from '@editorjs/editorjs'
import LessonHelp from '@/components/LessonHelp.vue' import LessonHelp from '@/components/LessonHelp.vue'
import { ChevronRight } from 'lucide-vue-next' import { ChevronRight } from 'lucide-vue-next'
import { updateDocumentTitle, createToast, getEditorTools } from '@/utils' import { createToast, getEditorTools } from '@/utils'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
const { brand } = sessionStore()
const editor = ref(null) const editor = ref(null)
const instructorEditor = ref(null) const instructorEditor = ref(null)
const user = inject('$user') const user = inject('$user')
@@ -492,14 +500,14 @@ const breadcrumbs = computed(() => {
return crumbs return crumbs
}) })
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: 'Lesson Editor', title: lessonDetails?.data?.lesson
description: 'Create and edit lessons for your course', ? lessonDetails.data.lesson.title
: 'New Lesson',
icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>
<style> <style>
.embed-tool__caption, .embed-tool__caption,

View File

@@ -65,12 +65,14 @@ import {
TabButtons, TabButtons,
Button, Button,
Tooltip, Tooltip,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { sessionStore } from '../stores/session'
import { computed, inject, ref, onMounted } from 'vue' import { computed, inject, ref, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { X } from 'lucide-vue-next' import { X } from 'lucide-vue-next'
import { updateDocumentTitle } from '@/utils'
const { brand } = sessionStore()
const user = inject('$user') const user = inject('$user')
const socket = inject('$socket') const socket = inject('$socket')
const activeTab = ref('Unread') const activeTab = ref('Unread')
@@ -145,14 +147,12 @@ const breadcrumbs = computed(() => {
return crumbs return crumbs
}) })
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: 'Notifications', title: 'Notifications',
description: 'All your notifications in one place.', icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>
<style> <style>
.notification strong { .notification strong {

View File

@@ -86,18 +86,24 @@
/> />
</template> </template>
<script setup> <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 { computed, inject, watch, ref, onMounted, watchEffect } from 'vue'
import { sessionStore } from '@/stores/session' 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 UserAvatar from '@/components/UserAvatar.vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import NoPermission from '@/components/NoPermission.vue' import NoPermission from '@/components/NoPermission.vue'
import { convertToTitleCase, updateDocumentTitle } from '@/utils' import { convertToTitleCase } from '@/utils'
import EditProfile from '@/components/Modals/EditProfile.vue' import EditProfile from '@/components/Modals/EditProfile.vue'
import EditCoverImage from '@/components/Modals/EditCoverImage.vue' import EditCoverImage from '@/components/Modals/EditCoverImage.vue'
const { user } = sessionStore() const { user, brand } = sessionStore()
const $user = inject('$user') const $user = inject('$user')
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
@@ -215,12 +221,10 @@ const breadcrumbs = computed(() => {
return crumbs return crumbs
}) })
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: profile.data?.full_name, title: profile.data?.full_name,
description: profile.data?.headline, icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>

View File

@@ -186,14 +186,17 @@ import {
ListHeader, ListHeader,
ListHeaderItem, ListHeaderItem,
ListSelectBanner, ListSelectBanner,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { Plus, Trash2 } from 'lucide-vue-next' import { Plus, Trash2 } from 'lucide-vue-next'
import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils/' import { showToast } from '@/utils/'
import Draggable from 'vuedraggable'
import { useRouter } from 'vue-router' 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 showDialog = ref(false)
const currentForm = ref(null) const currentForm = ref(null)
const course = ref(null) const course = ref(null)
@@ -364,4 +367,11 @@ const breadbrumbs = computed(() => {
}, },
] ]
}) })
usePageMeta(() => {
return {
title: program.doc?.title,
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -126,14 +126,17 @@ import {
createResource, createResource,
Dialog, Dialog,
FormControl, FormControl,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onMounted, ref } from 'vue' import { computed, inject, onMounted, ref } from 'vue'
import { BookOpen, Edit, Plus, LockKeyhole } from 'lucide-vue-next' import { BookOpen, Edit, Plus, LockKeyhole } from 'lucide-vue-next'
import CourseCard from '@/components/CourseCard.vue' import CourseCard from '@/components/CourseCard.vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { sessionStore } from '../stores/session'
import { showToast } from '@/utils' import { showToast } from '@/utils'
import { useSettings } from '@/stores/settings' import { useSettings } from '@/stores/settings'
const { brand } = sessionStore()
const user = inject('$user') const user = inject('$user')
const showDialog = ref(false) const showDialog = ref(false)
const router = useRouter() const router = useRouter()
@@ -210,4 +213,11 @@ const breadbrumbs = computed(() => [
label: 'Programs', label: 'Programs',
}, },
]) ])
usePageMeta(() => {
return {
title: __('Programs'),
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -197,6 +197,7 @@ import {
ListRowItem, ListRowItem,
ListSelectBanner, ListSelectBanner,
Button, Button,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
computed, computed,
@@ -207,11 +208,13 @@ import {
onBeforeUnmount, onBeforeUnmount,
watch, watch,
} from 'vue' } from 'vue'
import { sessionStore } from '../stores/session'
import { Plus, Trash2 } from 'lucide-vue-next' import { Plus, Trash2 } from 'lucide-vue-next'
import Question from '@/components/Modals/Question.vue'
import { showToast, updateDocumentTitle } from '@/utils' import { showToast, updateDocumentTitle } from '@/utils'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import Question from '@/components/Modals/Question.vue'
const { brand } = sessionStore()
const showQuestionModal = ref(false) const showQuestionModal = ref(false)
const currentQuestion = reactive({ const currentQuestion = reactive({
question: '', question: '',
@@ -453,12 +456,10 @@ const breadcrumbs = computed(() => {
return crumbs return crumbs
}) })
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: props.quizID == 'new' ? __('New Quiz') : quizDetails.data?.title, title: props.quizID == 'new' ? __('New Quiz') : quizDetails.data?.title,
description: __('Form to create and edit quizzes'), icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>

View File

@@ -14,11 +14,12 @@
</template> </template>
<script setup> <script setup>
import Quiz from '@/components/Quiz.vue' 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 { computed, inject, onMounted, ref } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { updateDocumentTitle } from '@/utils' import { sessionStore } from '../stores/session'
const { brand } = sessionStore()
const user = inject('$user') const user = inject('$user')
const router = useRouter() const router = useRouter()
const fromLesson = ref(false) const fromLesson = ref(false)
@@ -56,12 +57,10 @@ const breadcrumbs = computed(() => {
return [{ label: __('Quiz Submission') }, { label: title.data?.title }] return [{ label: __('Quiz Submission') }, { label: title.data?.title }]
}) })
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: title.data?.title, title: `${title.data?.title}`,
description: __('Quiz Submission'), icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>

View File

@@ -79,11 +79,14 @@ import {
FormControl, FormControl,
Button, Button,
Badge, Badge,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, onBeforeUnmount, onMounted, inject } from 'vue' import { computed, onBeforeUnmount, onMounted, inject } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { showToast } from '@/utils' import { showToast } from '@/utils'
import { sessionStore } from '@/stores/session'
const { brand } = sessionStore()
const router = useRouter() const router = useRouter()
const user = inject('$user') const user = inject('$user')
@@ -149,4 +152,11 @@ const saveSubmission = () => {
} }
) )
} }
usePageMeta(() => {
return {
title: `${submisisonDetails.doc.quiz_title}`,
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -40,6 +40,18 @@
</Button> </Button>
</div> </div>
</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> </template>
<script setup> <script setup>
import { import {
@@ -51,10 +63,14 @@ import {
ListRows, ListRows,
ListHeader, ListHeader,
ListHeaderItem, ListHeaderItem,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { BookOpen } from 'lucide-vue-next'
import { computed, onMounted, inject } from 'vue' import { computed, onMounted, inject } from 'vue'
import { sessionStore } from '../stores/session'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const { brand } = sessionStore()
const router = useRouter() const router = useRouter()
const user = inject('$user') const user = inject('$user')
@@ -105,4 +121,11 @@ const quizColumns = computed(() => {
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
return [{ label: __('Quiz Submissions') }] return [{ label: __('Quiz Submissions') }]
}) })
usePageMeta(() => {
return {
title: __('Quiz Submissions'),
icon: brand.favicon,
}
})
</script> </script>

View File

@@ -79,12 +79,14 @@ import {
ListRow, ListRow,
ListHeader, ListHeader,
ListHeaderItem, ListHeaderItem,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { computed, inject, onMounted } from 'vue' import { computed, inject, onMounted } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next' import { BookOpen, Plus } from 'lucide-vue-next'
import { updateDocumentTitle } from '@/utils' import { sessionStore } from '@/stores/session'
const { brand } = sessionStore()
const user = inject('$user') const user = inject('$user')
const router = useRouter() const router = useRouter()
@@ -143,12 +145,10 @@ const breadcrumbs = computed(() => {
] ]
}) })
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: __('Quizzes'), title: __('Quizzes'),
description: __('List of quizzes'), icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>

View File

@@ -39,11 +39,13 @@ import {
createDocumentResource, createDocumentResource,
createListResource, createListResource,
createResource, createResource,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { computed, inject, onBeforeMount, ref } from 'vue' import { computed, inject, onBeforeMount, ref } from 'vue'
import { useSidebar } from '@/stores/sidebar' import { useSidebar } from '@/stores/sidebar'
import { updateDocumentTitle } from '@/utils' import { sessionStore } from '../stores/session'
const { brand } = sessionStore()
const sidebarStore = useSidebar() const sidebarStore = useSidebar()
const user = inject('$user') const user = inject('$user')
const readyToRender = ref(false) const readyToRender = ref(false)
@@ -195,14 +197,10 @@ const breadcrumbs = computed(() => {
] ]
}) })
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: chapter?.doc?.title, title: chapter.doc?.title,
description: __('This is a chapter in the course {0}').format( icon: brand.favicon,
chapter?.doc?.course_title
),
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>

View File

@@ -117,9 +117,9 @@
</div> </div>
</template> </template>
<script setup> <script setup>
import { createResource, Breadcrumbs } from 'frappe-ui' import { createResource, Breadcrumbs, usePageMeta } from 'frappe-ui'
import { computed, inject } from 'vue' import { computed } from 'vue'
import { updateDocumentTitle } from '@/utils' import { sessionStore } from '../stores/session'
import { formatNumber } from '@/utils' import { formatNumber } from '@/utils'
import { Line, Pie } from 'vue-chartjs' import { Line, Pie } from 'vue-chartjs'
import { import {
@@ -154,7 +154,7 @@ import {
BookOpenCheck, BookOpenCheck,
} from 'lucide-vue-next' } from 'lucide-vue-next'
const dayjs = inject('$dayjs') const { brand } = sessionStore()
const breadcrumbs = computed(() => { const breadcrumbs = computed(() => {
return [ return [
@@ -317,12 +317,10 @@ const chartOptions = (isPie) => {
} }
} }
const pageMeta = computed(() => { usePageMeta(() => {
return { return {
title: 'Statistics', title: __('Statistics'),
description: 'Statistics of the platform', icon: brand.favicon,
} }
}) })
updateDocumentTitle(pageMeta)
</script> </script>

View File

@@ -2,10 +2,11 @@ import { defineStore } from 'pinia'
import { createResource } from 'frappe-ui' import { createResource } from 'frappe-ui'
import { usersStore } from './user' import { usersStore } from './user'
import router from '@/router' import router from '@/router'
import { ref, computed } from 'vue' import { computed, reactive, ref } from 'vue'
export const sessionStore = defineStore('lms-session', () => { export const sessionStore = defineStore('lms-session', () => {
let { userResource } = usersStore() let { userResource } = usersStore()
const brand = reactive({})
function sessionUser() { function sessionUser() {
let cookies = new URLSearchParams(document.cookie.split('; ').join('&')) let cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
@@ -46,7 +47,11 @@ export const sessionStore = defineStore('lms-session', () => {
cache: 'brand', cache: 'brand',
auto: true, auto: true,
onSuccess(data) { 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, isLoggedIn,
login, login,
logout, logout,
brand,
branding, branding,
sidebarSettings, sidebarSettings,
} }

View File

@@ -281,6 +281,7 @@ def get_job_details(job):
"type", "type",
"company_name", "company_name",
"company_logo", "company_logo",
"company_website",
"name", "name",
"creation", "creation",
"description", "description",

View File

@@ -11,66 +11,34 @@ import json
class CourseLesson(Document): class CourseLesson(Document):
def validate(self): def on_update(self):
# self.check_and_create_folder()
self.validate_quiz_id() self.validate_quiz_id()
def validate_quiz_id(self): def validate_quiz_id(self):
if self.quiz_id and not frappe.db.exists("LMS Quiz", self.quiz_id): if self.quiz_id and not frappe.db.exists("LMS Quiz", self.quiz_id):
frappe.throw(_("Invalid Quiz ID")) frappe.throw(_("Invalid Quiz ID"))
def on_update(self): if self.content:
dynamic_documents = ["Exercise", "Quiz"] self.save_lesson_details_in_quiz(self.content)
for section in dynamic_documents:
self.update_lesson_name_in_document(section)
def update_lesson_name_in_document(self, section): if self.instructor_content:
doctype_map = {"Exercise": "LMS Exercise", "Quiz": "LMS Quiz"} self.save_lesson_details_in_quiz(self.instructor_content)
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)
def update_orphan_documents(self, doctype, documents): def save_lesson_details_in_quiz(self, content):
"""Updates the documents that were previously part of this lesson, content = json.loads(self.content)
but not any more. for block in content.get("blocks"):
""" if block.get("type") == "quiz":
linked_documents = { quiz = block.get("data").get("quiz")
row["name"] for row in frappe.get_all(doctype, {"lesson": self.name}) if not frappe.db.exists("LMS Quiz", quiz):
} frappe.throw(_("Invalid Quiz ID in content"))
active_documents = set(documents) frappe.db.set_value(
orphan_documents = linked_documents - active_documents "LMS Quiz",
for name in orphan_documents: quiz,
ex = frappe.get_doc(doctype, name) {
ex.lesson = None "course": self.course,
ex.course = None "lesson": self.name,
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]
@frappe.whitelist() @frappe.whitelist()
@@ -102,7 +70,7 @@ def save_progress(lesson, course):
progress = get_course_progress(course) progress = get_course_progress(course)
capture_progress_for_analytics(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 = frappe.get_doc("LMS Enrollment", membership)
enrollment.progress = progress enrollment.progress = progress
enrollment.save() enrollment.save()

View File

@@ -136,9 +136,15 @@
"label": "Duration (in minutes)" "label": "Duration (in minutes)"
} }
], ],
"grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [
"modified": "2025-01-06 11:02:09.749207", {
"link_doctype": "LMS Quiz Submission",
"link_fieldname": "quiz"
}
],
"modified": "2025-04-07 15:03:48.525458",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Quiz", "name": "LMS Quiz",
@@ -190,10 +196,11 @@
"share": 1 "share": 1
} }
], ],
"row_format": "Dynamic",
"show_title_field_in_link": 1, "show_title_field_in_link": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -11,8 +11,6 @@ from fuzzywuzzy import fuzz
from lms.lms.doctype.course_lesson.course_lesson import save_progress from lms.lms.doctype.course_lesson.course_lesson import save_progress
from lms.lms.utils import ( from lms.lms.utils import (
generate_slug, generate_slug,
has_course_moderator_role,
has_course_instructor_role,
) )
from binascii import Error as BinasciiError from binascii import Error as BinasciiError
from frappe.utils.file_manager import safe_b64decode from frappe.utils.file_manager import safe_b64decode

View File

@@ -27,8 +27,9 @@
"signup_settings_tab", "signup_settings_tab",
"signup_settings_section", "signup_settings_section",
"column_break_9", "column_break_9",
"custom_signup_content",
"user_category", "user_category",
"disable_signup",
"custom_signup_content",
"sidebar_tab", "sidebar_tab",
"items_in_sidebar_section", "items_in_sidebar_section",
"courses", "courses",
@@ -104,7 +105,7 @@
"default": "0", "default": "0",
"fieldname": "user_category", "fieldname": "user_category",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Ask User Category during Signup" "label": "Identify User Persona"
}, },
{ {
"default": "0", "default": "0",
@@ -365,12 +366,19 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Payment Reminder Template", "label": "Payment Reminder Template",
"options": "Email Template" "options": "Email Template"
},
{
"default": "0",
"fieldname": "disable_signup",
"fieldtype": "Check",
"label": "Disable Signup"
} }
], ],
"grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-02-11 11:29:43.412897", "modified": "2025-04-07 18:05:52.000651",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "LMS Settings", "name": "LMS Settings",
@@ -394,8 +402,9 @@
"share": 1 "share": 1
} }
], ],
"row_format": "Dynamic",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -10,6 +10,7 @@ from frappe.utils import get_url_to_list
class LMSSettings(Document): class LMSSettings(Document):
def validate(self): def validate(self):
self.validate_google_settings() self.validate_google_settings()
self.validate_signup()
def validate_google_settings(self): def validate_google_settings(self):
if self.send_calendar_invite_for_evaluations: 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() @frappe.whitelist()
def check_payments_app(): def check_payments_app():

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

6235
lms/locale/pt_BR.po Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@
<label class="form-label sr-only"> {{ _("User Category") }} </label> <label class="form-label sr-only"> {{ _("User Category") }} </label>
<div class="control-input-wrapper"> <div class="control-input-wrapper">
<div class="control-input flex align-center"> <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> class="input-with-feedback form-control ellipsis" data-fieldtype="Select" required>
<option value=""> {{ _("Category") }} </option> <option value=""> {{ _("Category") }} </option>
<option value="Business Owner"> {{ _("Business Owner") }} </option> <option value="Business Owner"> {{ _("Business Owner") }} </option>

View File

@@ -10,25 +10,58 @@ no_cache = 1
def get_context(): def get_context():
app_path = frappe.form_dict.get("app_path") app_path = frappe.form_dict.get("app_path")
context = frappe._dict() favicon = (
if app_path: frappe.db.get_single_value("Website Settings", "favicon")
context.meta = get_meta(app_path) or "/assets/lms/frontend/favicon.png"
else: )
context.meta = {} title = frappe.db.get_single_value("Website Settings", "app_name") or "Frappe Learning"
csrf_token = frappe.sessions.get_csrf_token() csrf_token = frappe.sessions.get_csrf_token()
frappe.db.commit() # nosemgrep frappe.db.commit()
context = frappe._dict()
context.csrf_token = csrf_token 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") capture("active_site", "lms")
context.favicon = frappe.db.get_single_value("Website Settings", "favicon") context.title = title
context.favicon = favicon
return context 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": if app_path == "courses":
return { return {
"title": _("Course List"), "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", "description": "This page lists all the courses published on our website",
"keywords": "All Courses, Courses, Learn", "keywords": "All Courses, Courses, Learn",
"link": "/courses", "link": "/courses",
@@ -47,13 +80,18 @@ def get_meta(app_path):
course = frappe.db.get_value( course = frappe.db.get_value(
"LMS Course", "LMS Course",
course_name, course_name,
["title", "image", "short_introduction", "tags"], ["title", "image", "description", "tags"],
as_dict=True, as_dict=True,
) )
if course.description:
soup = BeautifulSoup(course.description, "html.parser")
course.description = soup.get_text()
return { return {
"title": course.title, "title": course.title,
"image": course.image, "image": course.image,
"description": course.short_introduction, "description": course.description,
"keywords": course.tags, "keywords": course.tags,
"link": f"/courses/{course_name}", "link": f"/courses/{course_name}",
} }
@@ -61,7 +99,7 @@ def get_meta(app_path):
if app_path == "batches": if app_path == "batches":
return { return {
"title": _("Batches"), "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", "description": "This page lists all the batches published on our website",
"keywords": "All Batches, Batches, Learn", "keywords": "All Batches, Batches, Learn",
"link": "/batches", "link": "/batches",
@@ -71,13 +109,18 @@ def get_meta(app_path):
batch = frappe.db.get_value( batch = frappe.db.get_value(
"LMS Batch", "LMS Batch",
batch_name, batch_name,
["title", "meta_image", "description", "category", "medium"], ["title", "meta_image", "batch_details", "category", "medium"],
as_dict=True, as_dict=True,
) )
if batch.batch_details:
soup = BeautifulSoup(batch.batch_details, "html.parser")
batch.batch_details = soup.get_text()
return { return {
"title": batch.title, "title": batch.title,
"image": batch.meta_image, "image": batch.meta_image,
"description": batch.description, "description": batch.batch_details,
"keywords": f"{batch.category} {batch.medium}", "keywords": f"{batch.category} {batch.medium}",
"link": f"/batches/details/{batch_name}", "link": f"/batches/details/{batch_name}",
} }
@@ -87,7 +130,7 @@ def get_meta(app_path):
if "new/edit" in app_path: if "new/edit" in app_path:
return { return {
"title": _("New Batch"), "title": _("New Batch"),
"image": frappe.db.get_single_value("Website Settings", "banner_image"), "image": favicon,
"description": "Create a new batch", "description": "Create a new batch",
"keywords": "New Batch, Create Batch", "keywords": "New Batch, Create Batch",
"link": "/lms/batches/new/edit", "link": "/lms/batches/new/edit",
@@ -95,13 +138,18 @@ def get_meta(app_path):
batch = frappe.db.get_value( batch = frappe.db.get_value(
"LMS Batch", "LMS Batch",
batch_name, batch_name,
["title", "meta_image", "description", "category", "medium"], ["title", "meta_image", "batch_details", "category", "medium"],
as_dict=True, as_dict=True,
) )
if batch.batch_details:
soup = BeautifulSoup(batch.batch_details, "html.parser")
batch.batch_details = soup.get_text()
return { return {
"title": batch.title, "title": batch.title,
"image": batch.meta_image, "image": batch.meta_image,
"description": batch.description, "description": batch.batch_details,
"keywords": f"{batch.category} {batch.medium}", "keywords": f"{batch.category} {batch.medium}",
"link": f"/batches/{batch_name}", "link": f"/batches/{batch_name}",
} }
@@ -109,7 +157,7 @@ def get_meta(app_path):
if app_path == "job-openings": if app_path == "job-openings":
return { return {
"title": _("Job Openings"), "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", "description": "This page lists all the job openings published on our website",
"keywords": "Job Openings, Jobs, Vacancies", "keywords": "Job Openings, Jobs, Vacancies",
"link": "/job-openings", "link": "/job-openings",
@@ -120,13 +168,13 @@ def get_meta(app_path):
job_opening = frappe.db.get_value( job_opening = frappe.db.get_value(
"Job Opportunity", "Job Opportunity",
job_opening_name, job_opening_name,
["job_title", "company_logo", "company_name"], ["job_title", "company_logo", "description"],
as_dict=True, as_dict=True,
) )
return { return {
"title": job_opening.job_title, "title": job_opening.job_title,
"image": job_opening.company_logo, "image": job_opening.company_logo,
"description": job_opening.company_name, "description": job_opening.description,
"keywords": "Job Openings, Jobs, Vacancies", "keywords": "Job Openings, Jobs, Vacancies",
"link": f"/job-openings/{job_opening_name}", "link": f"/job-openings/{job_opening_name}",
} }
@@ -134,7 +182,7 @@ def get_meta(app_path):
if app_path == "statistics": if app_path == "statistics":
return { return {
"title": _("Statistics"), "title": _("Statistics"),
"image": frappe.db.get_single_value("Website Settings", "banner_image"), "image": favicon,
"description": "This page lists all the statistics of this platform", "description": "This page lists all the statistics of this platform",
"keywords": "Enrollment Count, Completion, Signups", "keywords": "Enrollment Count, Completion, Signups",
"link": "/statistics", "link": "/statistics",
@@ -179,3 +227,64 @@ def get_meta(app_path):
"keywords": f"{badge.title}, {badge.description}", "keywords": f"{badge.title}, {badge.description}",
"link": f"/badges/{badgeName}/{email}", "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",
}