Compare commits

...

48 Commits

Author SHA1 Message Date
Frappe PR Bot
04aff8d149 chore(release): Bumped to Version 2.27.0 2025-04-10 10:38:37 +00:00
Jannat Patel
e88bdd818d Merge pull request #1422 from pateljannat/issues-89
fix: don't update onboarding status if user is not system manager
2025-04-10 15:55:32 +05:30
Jannat Patel
1a5d8ce07e fix: don't update onboarding status if user is not system manager 2025-04-10 15:37:59 +05:30
Jannat Patel
8e405bc8eb Merge pull request #1416 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-04-09 15:01:24 +05:30
Jannat Patel
23e2a153c9 chore: German translations 2025-04-09 10:03:41 +05:30
Jannat Patel
85a0949488 Merge pull request #1415 from pateljannat/jobs-improvements
fix: new ui for job list
2025-04-08 22:53:30 +05:30
Jannat Patel
57b6433dc0 fix: new ui for job list 2025-04-08 22:39:42 +05:30
Jannat Patel
1b43e1be44 fix: added back csrf_token 2025-04-08 21:33:52 +05:30
Jannat Patel
d6738b86c9 Merge pull request #1414 from pateljannat/seo-improvements
fix: seo improvements
2025-04-08 21:09:54 +05:30
Jannat Patel
a5325cef44 chore: renamed lint jobs 2025-04-08 21:03:30 +05:30
Jannat Patel
cc917f3d83 chore: bumped up pre commit action version to 3.0. 2025-04-08 21:00:50 +05:30
Jannat Patel
492917ea40 chore: added commit lint rules 2025-04-08 20:53:44 +05:30
Jannat Patel
78263185a1 chore: cached pip for linters 2025-04-08 20:31:42 +05:30
Jannat Patel
ee7aa9d58b feat: fetch meta tags from website route meta 2025-04-08 19:37:16 +05:30
Jannat Patel
a7112937de fix: changed course and batch meta description 2025-04-08 18:21:28 +05:30
Jannat Patel
a8d4572aef fix: add app_name as document title 2025-04-08 18:12:21 +05:30
Jannat Patel
45c530e53a Merge pull request #1413 from pateljannat/improve-page-meta
fix: persistent favicon for all pages
2025-04-08 11:52:23 +05:30
Jannat Patel
e0bcce5e6e fix: show learning logo as favicon if its missing in website settings 2025-04-08 11:45:51 +05:30
Jannat Patel
8346ec8525 fix: import usePageMeta for QuizSubmission 2025-04-08 11:13:46 +05:30
Jannat Patel
5d1673bad8 fix: persistent favicon for all pages 2025-04-08 10:57:02 +05:30
Jannat Patel
a33328e11d Merge pull request #1412 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-04-08 10:27:35 +05:30
Jannat Patel
3efa326684 chore: Esperanto translations 2025-04-08 09:28:34 +05:30
Jannat Patel
196fead1e0 chore: Croatian translations 2025-04-08 09:28:33 +05:30
Jannat Patel
b8ce04e9fe chore: Thai translations 2025-04-08 09:28:31 +05:30
Jannat Patel
6369dfd65c chore: Portuguese, Brazilian translations 2025-04-08 09:28:30 +05:30
Jannat Patel
f4da56adf9 chore: Bosnian translations 2025-04-08 09:28:29 +05:30
Jannat Patel
0987a91bfc chore: Persian translations 2025-04-08 09:28:27 +05:30
Jannat Patel
9f23a56cf4 chore: Chinese Simplified translations 2025-04-08 09:28:26 +05:30
Jannat Patel
34a4754767 chore: Turkish translations 2025-04-08 09:28:25 +05:30
Jannat Patel
b88de74552 chore: Swedish translations 2025-04-08 09:28:24 +05:30
Jannat Patel
45ac682c7f chore: Russian translations 2025-04-08 09:28:22 +05:30
Jannat Patel
b753d366bf chore: Polish translations 2025-04-08 09:28:21 +05:30
Jannat Patel
06c598886e chore: Hungarian translations 2025-04-08 09:28:20 +05:30
Jannat Patel
52b0b7f8dc chore: German translations 2025-04-08 09:28:18 +05:30
Jannat Patel
656b3b2ebe chore: Arabic translations 2025-04-08 09:28:17 +05:30
Jannat Patel
6bdfbde23f chore: Spanish translations 2025-04-08 09:28:16 +05:30
Jannat Patel
1b9f5eebc0 chore: French translations 2025-04-08 09:28:15 +05:30
Jannat Patel
1f37da08b4 Merge pull request #1411 from pateljannat/disable-signup-settings
feat: signups can now be enabled/disabled from portal settings
2025-04-07 18:22:57 +05:30
Jannat Patel
5bc44e6fe5 feat: signups can now be enabled/disabled from portal settings 2025-04-07 18:15:30 +05:30
Jannat Patel
c70da08078 Merge pull request #1410 from pateljannat/quiz-issue
fix: save lesson details in quiz
2025-04-07 15:19:15 +05:30
Jannat Patel
7600fb14e1 Merge pull request #1409 from frappe/pot_develop_2025-04-04
chore: update POT file
2025-04-07 15:10:55 +05:30
Jannat Patel
e2fdf2042e Merge pull request #1406 from frappe/l10n_develop2
chore: sync translations from crowdin
2025-04-07 15:10:38 +05:30
Jannat Patel
8477d6b9ed fix: save lesson details in quiz 2025-04-07 15:09:31 +05:30
Jannat Patel
241df63334 chore: Persian translations 2025-04-07 09:31:08 +05:30
Jannat Patel
7131de8a2a chore: Persian translations 2025-04-06 09:34:34 +05:30
frappe-pr-bot
473a799f58 chore: update POT file 2025-04-04 16:04:26 +00:00
Jannat Patel
6c9fe85170 chore: Portuguese, Brazilian translations 2025-04-01 09:07:24 +05:30
Jannat Patel
2c5d2db340 chore: Chinese Simplified translations 2025-03-29 08:33:34 +05:30
83 changed files with 10843 additions and 2690 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

@@ -118,6 +118,23 @@ import { ref, watch, reactive, inject } from 'vue'
import { RefreshCw, Plus, X } from 'lucide-vue-next' import { RefreshCw, Plus, X } from 'lucide-vue-next'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
interface User {
data: {
email: string
name: string
enabled: boolean
user_image: string
full_name: string
user_type: ['System User', 'Website User']
username: string
is_moderator: boolean
is_system_manager: boolean
is_evaluator: boolean
is_instructor: boolean
is_fc_site: boolean
}
}
const router = useRouter() const router = useRouter()
const show = defineModel('show') const show = defineModel('show')
const search = ref('') const search = ref('')
@@ -126,6 +143,7 @@ const memberList = ref([])
const hasNextPage = ref(false) const hasNextPage = ref(false)
const showForm = ref(false) const showForm = ref(false)
const dayjs = inject('$dayjs') const dayjs = inject('$dayjs')
const user = inject<User | null>('$user')
const { updateOnboardingStep } = useOnboarding('learning') const { updateOnboardingStep } = useOnboarding('learning')
const member = reactive({ const member = reactive({
@@ -187,7 +205,9 @@ const newMember = createResource({
auto: false, auto: false,
onSuccess(data) { onSuccess(data) {
show.value = false show.value = false
updateOnboardingStep('invite_students')
if (user?.data?.is_system_manager) updateOnboardingStep('invite_students')
router.push({ router.push({
name: 'Profile', name: 'Profile',
params: { params: {

View File

@@ -32,7 +32,7 @@
</template> </template>
<script setup> <script setup>
import { Dialog, createResource } from 'frappe-ui' import { Dialog, createResource } from 'frappe-ui'
import { ref } from 'vue' import { ref, inject } from 'vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils' import { showToast } from '@/utils'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
@@ -41,6 +41,7 @@ import { useSettings } from '@/stores/settings'
const show = defineModel() const show = defineModel()
const course = ref(null) const course = ref(null)
const evaluator = ref(null) const evaluator = ref(null)
const user = inject('$user')
const courses = defineModel('courses') const courses = defineModel('courses')
const { updateOnboardingStep } = useOnboarding('learning') const { updateOnboardingStep } = useOnboarding('learning')
const settingsStore = useSettings() const settingsStore = useSettings()
@@ -73,9 +74,11 @@ const addCourse = (close) => {
{}, {},
{ {
onSuccess() { onSuccess() {
courses.value.reload() if (user.data?.is_system_manager)
updateOnboardingStep('add_batch_course') updateOnboardingStep('add_batch_course')
close() close()
courses.value.reload()
course.value = null course.value = null
evaluator.value = null evaluator.value = null
}, },

View File

@@ -77,7 +77,7 @@ import {
FormControl, FormControl,
Switch, Switch,
} from 'frappe-ui' } from 'frappe-ui'
import { reactive, watch } from 'vue' import { reactive, watch, inject } from 'vue'
import { showToast, getFileSize } from '@/utils/' import { showToast, getFileSize } from '@/utils/'
import { capture } from '@/telemetry' import { capture } from '@/telemetry'
import { FileText, X } from 'lucide-vue-next' import { FileText, X } from 'lucide-vue-next'
@@ -85,6 +85,7 @@ import { useOnboarding } from 'frappe-ui/frappe'
const show = defineModel() const show = defineModel()
const outline = defineModel('outline') const outline = defineModel('outline')
const user = inject('$user')
const { updateOnboardingStep } = useOnboarding('learning') const { updateOnboardingStep } = useOnboarding('learning')
const props = defineProps({ const props = defineProps({
@@ -139,8 +140,10 @@ const addChapter = async (close) => {
return validateChapter() return validateChapter()
}, },
onSuccess: (data) => { onSuccess: (data) => {
if (user.data?.is_system_manager)
updateOnboardingStep('create_first_chapter')
capture('chapter_created') capture('chapter_created')
updateOnboardingStep('create_first_chapter')
chapterReference.submit( chapterReference.submit(
{ name: data.name }, { name: data.name },
{ {

View File

@@ -106,7 +106,7 @@
</template> </template>
<script setup> <script setup>
import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui' import { Dialog, FormControl, TextEditor, createResource } from 'frappe-ui'
import { computed, watch, reactive, ref } from 'vue' import { computed, watch, reactive, ref, inject } from 'vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils' import { showToast } from '@/utils'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
@@ -115,11 +115,12 @@ const show = defineModel()
const quiz = defineModel('quiz') const quiz = defineModel('quiz')
const questionType = ref(null) const questionType = ref(null)
const editMode = ref(false) const editMode = ref(false)
const user = inject('$user')
const { updateOnboardingStep } = useOnboarding('learning') const { updateOnboardingStep } = useOnboarding('learning')
const existingQuestion = reactive({ const existingQuestion = reactive({
question: '', question: '',
marks: 0, marks: 1,
}) })
const question = reactive({ const question = reactive({
question: '', question: '',
@@ -262,8 +263,10 @@ const addQuestionRow = (question, close) => {
}, },
{ {
onSuccess() { onSuccess() {
if (user.data?.is_system_manager)
updateOnboardingStep('create_first_quiz')
show.value = false show.value = false
updateOnboardingStep('create_first_quiz')
showToast(__('Success'), __('Question added successfully'), 'check') showToast(__('Success'), __('Question added successfully'), 'check')
quiz.value.reload() quiz.value.reload()
close() close()

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

@@ -26,13 +26,14 @@
</template> </template>
<script setup> <script setup>
import { Dialog, createResource } from 'frappe-ui' import { Dialog, createResource } from 'frappe-ui'
import { ref } from 'vue' import { ref, inject } from 'vue'
import Link from '@/components/Controls/Link.vue' import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils' import { showToast } from '@/utils'
import { useOnboarding } from 'frappe-ui/frappe' import { useOnboarding } from 'frappe-ui/frappe'
const students = defineModel('reloadStudents') const students = defineModel('reloadStudents')
const student = ref() const student = ref()
const user = inject('$user')
const { updateOnboardingStep } = useOnboarding('learning') const { updateOnboardingStep } = useOnboarding('learning')
const show = defineModel() const show = defineModel()
@@ -61,9 +62,11 @@ const addStudent = (close) => {
{}, {},
{ {
onSuccess() { onSuccess() {
if (user.data?.is_system_manager)
updateOnboardingStep('add_batch_student')
students.value.reload() students.value.reload()
student.value = null student.value = null
updateOnboardingStep('add_batch_student')
close() close()
}, },
onError(err) { onError(err) {

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({
@@ -427,10 +430,13 @@ const createNewBatch = () => {
{}, {},
{ {
onSuccess(data) { onSuccess(data) {
if (user.data?.is_system_manager) {
updateOnboardingStep('create_first_batch', true, false, () => {
localStorage.setItem('firstBatch', data.name)
})
}
capture('batch_created') capture('batch_created')
updateOnboardingStep('create_first_batch', true, false, () => {
localStorage.setItem('firstBatch', data.name)
})
router.push({ router.push({
name: 'BatchDetail', name: 'BatchDetail',
params: { params: {
@@ -505,4 +511,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

@@ -3,15 +3,11 @@
<div class="grid md:grid-cols-[70%,30%] h-full"> <div class="grid md:grid-cols-[70%,30%] h-full">
<div> <div>
<header <header
class="sticky top-0 z-10 group flex flex-col md:flex-row md:items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5" class="sticky top-0 z-10 flex flex-col md:flex-row md:items-center justify-between border-b bg-surface-white px-3 py-2.5 sm:px-5"
> >
<Breadcrumbs class="h-7" :items="breadcrumbs" /> <Breadcrumbs class="h-7" :items="breadcrumbs" />
<div class="flex items-center mt-3 md:mt-0"> <div class="flex items-center mt-3 md:mt-0">
<Button <Button v-if="courseResource.data?.name" @click="trashCourse()">
v-if="courseResource.data?.name"
@click="trashCourse()"
class="invisible group-hover:visible"
>
<template #icon> <template #icon>
<Trash2 class="w-4 h-4 stroke-1.5" /> <Trash2 class="w-4 h-4 stroke-1.5" />
</template> </template>
@@ -253,6 +249,7 @@ import {
createResource, createResource,
FormControl, FormControl,
FileUploader, FileUploader,
usePageMeta,
} from 'frappe-ui' } from 'frappe-ui'
import { import {
inject, inject,
@@ -264,18 +261,20 @@ import {
watch, watch,
getCurrentInstance, getCurrentInstance,
} from 'vue' } from 'vue'
import { showToast, updateDocumentTitle } from '@/utils' import { showToast } 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()
@@ -401,7 +400,7 @@ const courseResource = createResource({
'paid_course', 'paid_course',
'featured', 'featured',
'enable_certification', 'enable_certification',
'paid_certifiate', 'paid_certificate',
] ]
for (let idx in checkboxes) { for (let idx in checkboxes) {
let key = checkboxes[idx] let key = checkboxes[idx]
@@ -444,11 +443,14 @@ const submitCourse = () => {
} else { } else {
courseCreationResource.submit(course, { courseCreationResource.submit(course, {
onSuccess(data) { onSuccess(data) {
if (user.data?.is_system_manager) {
updateOnboardingStep('create_first_course', true, false, () => {
localStorage.setItem('firstCourse', data.name)
})
}
capture('course_created') capture('course_created')
showToast('Success', 'Course created successfully', 'check') showToast('Success', 'Course created successfully', 'check')
updateOnboardingStep('create_first_course', true, false, () => {
localStorage.setItem('firstCourse', data.name)
})
router.push({ router.push({
name: 'CourseForm', name: 'CourseForm',
params: { courseName: data.name }, params: { courseName: data.name },
@@ -574,12 +576,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

@@ -4,7 +4,7 @@
> >
<Breadcrumbs :items="breadcrumbs" /> <Breadcrumbs :items="breadcrumbs" />
<router-link <router-link
v-if="user.data?.is_moderator" v-if="canCreateCourse()"
:to="{ :to="{
name: 'CourseForm', name: 'CourseForm',
params: { courseName: 'new' }, params: { courseName: 'new' },
@@ -100,10 +100,12 @@ 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 { canCreateCourse } from '@/utils'
import CourseCard from '@/components/CourseCard.vue' import CourseCard from '@/components/CourseCard.vue'
const user = inject('$user') const user = inject('$user')
@@ -116,6 +118,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 +306,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 v-if="jobs.data?.length" 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"
> >
@@ -58,10 +58,7 @@
</div> </div>
</div> </div>
<div <div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
v-if="jobs.data?.length"
class="grid grid-cols-1 lg:grid-cols-2 gap-5"
>
<router-link <router-link
v-for="job in jobs.data" v-for="job in jobs.data"
:to="{ :to="{
@@ -73,22 +70,42 @@
<JobCard :job="job" /> <JobCard :job="job" />
</router-link> </router-link>
</div> </div>
<div v-else class="text-ink-gray-7 italic p-5 w-fit mx-auto"> </div>
{{ __('No jobs posted') }} <div
v-else
class="flex flex-col items-center justify-center text-sm text-ink-gray-5 mt-48"
>
<Laptop class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-lg font-medium mb-1">
{{ __('No jobs found') }}
</div>
<div class="leading-5 w-2/5 text-center">
{{
__(
'There are no jobs available at the moment. Open a job opportunity or check here again later.'
)
}}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { Button, Breadcrumbs, createResource, FormControl } from 'frappe-ui' import {
import { Plus, Search } from 'lucide-vue-next' Button,
Breadcrumbs,
createResource,
FormControl,
usePageMeta,
} from 'frappe-ui'
import { Laptop, 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 +164,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')
@@ -394,8 +402,10 @@ const createNewLesson = () => {
{ lesson: data.name }, { lesson: data.name },
{ {
onSuccess() { onSuccess() {
if (user.data?.is_system_manager)
updateOnboardingStep('create_first_lesson')
capture('lesson_created') capture('lesson_created')
updateOnboardingStep('create_first_lesson')
showToast('Success', 'Lesson created successfully', 'check') showToast('Success', 'Lesson created successfully', 'check')
lessonDetails.reload() lessonDetails.reload()
}, },
@@ -492,14 +502,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

@@ -9,15 +9,9 @@ export const useSettings = defineStore('settings', () => {
const activeTab = ref(null) const activeTab = ref(null)
const learningPaths = createResource({ const learningPaths = createResource({
url: 'frappe.client.get_single_value', url: 'lms.lms.api.is_learning_path_enabled',
makeParams(values) { auto: true,
return { cache: ['learningPath'],
doctype: 'LMS Settings',
field: 'enable_learning_paths',
}
},
auto: isLoggedIn ? true : false,
cache: ['learningPaths'],
}) })
const allowGuestAccess = createResource({ const allowGuestAccess = createResource({
@@ -26,12 +20,6 @@ export const useSettings = defineStore('settings', () => {
cache: ['allowGuestAccess'], cache: ['allowGuestAccess'],
}) })
/* const onboardingDetails = createResource({
url: 'lms.lms.utils.is_onboarding_complete',
auto: isLoggedIn ? true : false,
cache: ['onboardingDetails'],
}) */
return { return {
isSettingsOpen, isSettingsOpen,
activeTab, activeTab,

View File

@@ -14,6 +14,7 @@ import dayjs from '@/utils/dayjs'
import Embed from '@editorjs/embed' import Embed from '@editorjs/embed'
import SimpleImage from '@editorjs/simple-image' import SimpleImage from '@editorjs/simple-image'
import Table from '@editorjs/table' import Table from '@editorjs/table'
import { usersStore } from '../stores/user'
export function createToast(options) { export function createToast(options) {
toast({ toast({
@@ -567,3 +568,8 @@ export const escapeHTML = (text) => {
(char) => escape_html_mapping[char] || char (char) => escape_html_mapping[char] || char
) )
} }
export const canCreateCourse = () => {
const { userResource } = usersStore()
return userResource.data?.is_instructor || userResource.data?.is_moderator
}

View File

@@ -25,7 +25,7 @@ export default defineConfig({
}), }),
], ],
server: { server: {
allowedHosts: ['fs', 'onb1'], allowedHosts: ['fs', 'onb2'],
}, },
resolve: { resolve: {
alias: { alias: {

View File

@@ -1 +1 @@
__version__ = "2.26.0" __version__ = "2.27.0"

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",
@@ -1258,6 +1259,11 @@ def is_guest_allowed():
return frappe.get_cached_value("LMS Settings", None, "allow_guest_access") return frappe.get_cached_value("LMS Settings", None, "allow_guest_access")
@frappe.whitelist(allow_guest=True)
def is_learning_path_enabled():
return frappe.get_cached_value("LMS Settings", None, "enable_learning_paths")
@frappe.whitelist() @frappe.whitelist()
def cancel_evaluation(evaluation): def cancel_evaluation(evaluation):
evaluation = frappe._dict(evaluation) evaluation = frappe._dict(evaluation)

View File

@@ -161,7 +161,7 @@
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2024-11-14 13:46:56.838659", "modified": "2025-04-10 15:19:22.400932",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "LMS", "module": "LMS",
"name": "Course Lesson", "name": "Course Lesson",
@@ -189,14 +189,28 @@
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "LMS Student", "role": "Course Creator",
"select": 1,
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Moderator",
"select": 1, "select": 1,
"share": 1, "share": 1,
"write": 1 "write": 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

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

@@ -4,7 +4,7 @@
<br> <br>
<p> {{ _(" Please evaluate and grade it.") }} </p> <p> {{ _(" Please evaluate and grade it.") }} </p>
<br>` <br>`
<a href="/assignment-submission/{{ assignment_name }}/{{ submission_name }}"> <a href="/lms/assignment-submission/{{ assignment_name }}/{{ submission_name }}">
{{ _("Open Assignment") }} {{ _("Open Assignment") }}
</a> </a>

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",
}