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 10604 additions and 2550 deletions

View File

@@ -7,8 +7,27 @@ on:
branches: [ main ]
jobs:
commit-lint:
name: 'Semantic Commits'
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 200
- uses: actions/setup-node@v4
with:
node-version: 20
check-latest: true
- name: Check commit titles
run: |
npm install @commitlint/cli @commitlint/config-conventional
npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }}
linters:
name: Semantic Commits
name: Semgrep Rules
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
@@ -20,8 +39,17 @@ jobs:
with:
python-version: '3.10'
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Install and Run Pre-commit
uses: pre-commit/action@v2.0.3
uses: pre-commit/action@v3.0.1
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules

View File

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

26
commitlint.config.js Normal file
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">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="{{ favicon or '/assets/lms/frontend/favicon.png' }}" />
<link rel="icon" href="{{ favicon }}" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Frappe Learning</title>
<title>{{ title }}</title>
<meta name="title" content="{{ meta.title }}" />
<meta name="image" content="{{ meta.image }}" />
<meta name="description" content="{{ meta.description }}" />
@@ -23,17 +23,6 @@
<p>
{{ meta.description }}
</p>
<p>
The content here is just for seo purposes. The actual content will be loaded in a few seconds.
</p>
<p>
Seo checks if a page has more than 300 words. So, here are some more words to make it more than 300 words.
Page descriptions are the HTML meta tags that provide a brief summary of a web page.
Search engines use meta descriptions to help identify the page's topic - they don't use them to rank the page, but they do use them to determine whether or not to display the page in search results.
Meta descriptions are important because they're often the first thing people see when they're deciding which search result to click on.
They're also important because they can help improve your click-through rate (CTR) from search results.
A good meta description can entice people to click on your page instead of someone else's.
</p>
<a href="{{ meta.link }}">Know More</a>
</div>
</div>
@@ -41,9 +30,8 @@
<div id="popovers"></div>
<script>
window.csrf_token = '{{ csrf_token }}'
window.setup_complete = '{{ setup_complete }}'
document.getElementById('seo-content').style.display = 'none';
window.csrf_token = '{{ csrf_token }}'
</script>
<script type="module" src="/src/main.js"></script>
</body>

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()
aceEditor?.setValue(value)
aceEditor?.clearSelection()
console.log(isDark.value)
aceEditor?.setTheme(isDark.value ? 'ace/theme/twilight' : 'ace/theme/chrome')
props.autofocus && aceEditor?.focus()
if (resetHistory) {

View File

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

View File

@@ -1,36 +1,18 @@
<template>
<svg
width="118"
height="118"
viewBox="0 0 118 118"
width="80"
height="79"
viewBox="0 0 80 79"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M93.9278 0H23.1013C10.3428 0 0 10.3428 0 23.1013V93.9278C0 106.686 10.3428 117.029 23.1013 117.029H93.9278C106.686 117.029 117.029 106.686 117.029 93.9278V23.1013C117.029 10.3428 106.686 0 93.9278 0Z"
fill="url(#paint0_radial_174_336)"
d="M57.1285 0.580383H22.8514C10.2309 0.580383 0 10.5649 0 22.8815V56.3332C0 68.6497 10.2309 78.6343 22.8514 78.6343H57.1285C69.749 78.6343 79.9799 68.6497 79.9799 56.3332V22.8815C79.9799 10.5649 69.749 0.580383 57.1285 0.580383Z"
fill="#0E7159"
/>
<path
d="M93.9278 0H23.1013C10.3428 0 0 10.3428 0 23.1013V93.9278C0 106.686 10.3428 117.029 23.1013 117.029H93.9278C106.686 117.029 117.029 106.686 117.029 93.9278V23.1013C117.029 10.3428 106.686 0 93.9278 0Z"
fill="#0B3D3D"
fill-opacity="0.8"
d="M62.8434 23.6906L60.7869 23.1052C53.6744 21.0702 45.9048 22.4641 39.992 26.8128C35.8502 23.7742 30.7943 22.1854 25.7099 22.2133H17.1406V27.8163H25.7099C29.6232 27.8163 33.508 29.015 36.6787 31.3845L39.992 33.8377L43.3056 31.3845C47.2475 28.4575 52.3032 27.2588 57.1306 28.0393V50.647C51.1035 49.9223 44.9051 51.4834 39.992 55.0795C35.8502 52.0688 30.8515 50.4798 25.7671 50.4798C24.7959 50.4798 23.8247 50.5355 22.8535 50.647V35.0642H17.1406V57.0588H62.8434V23.7185V23.6906Z"
fill="white"
/>
<path
d="M95.1879 33.1294L91.4077 32.0268C80.1721 28.7716 67.9389 30.9242 58.5409 37.7496C52.083 33.0769 43.9975 30.5042 36.1746 30.5042H21.8938V41.0048H36.2796C42.2649 41.0048 48.1978 42.9999 52.923 46.6226L58.5934 50.9279L64.2637 46.6226C70.144 42.1599 77.5469 40.2698 84.7923 41.2673V76.1818C75.5518 75.2367 66.2063 77.7044 58.6459 83.2172C51.0854 77.7044 41.6349 75.2367 32.4994 76.1818V52.8705H21.9988V86.4724H95.3454V33.1294H95.1879Z"
fill="#58FF9B"
/>
<defs>
<radialGradient
id="paint0_radial_174_336"
cx="0"
cy="0"
r="1"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(117.24 -101.5) rotate(105.042) scale(226.282)"
>
<stop offset="0.445162" stop-color="#1F7676" />
<stop offset="1" stop-color="#0A4B4B" />
</radialGradient>
</defs>
</svg>
</template>

View File

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

View File

@@ -118,6 +118,23 @@ import { ref, watch, reactive, inject } from 'vue'
import { RefreshCw, Plus, X } from 'lucide-vue-next'
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 show = defineModel('show')
const search = ref('')
@@ -126,6 +143,7 @@ const memberList = ref([])
const hasNextPage = ref(false)
const showForm = ref(false)
const dayjs = inject('$dayjs')
const user = inject<User | null>('$user')
const { updateOnboardingStep } = useOnboarding('learning')
const member = reactive({
@@ -187,7 +205,9 @@ const newMember = createResource({
auto: false,
onSuccess(data) {
show.value = false
updateOnboardingStep('invite_students')
if (user?.data?.is_system_manager) updateOnboardingStep('invite_students')
router.push({
name: 'Profile',
params: {

View File

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

View File

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

View File

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

View File

@@ -328,19 +328,26 @@ const tabsStructure = computed(() => {
icon: 'LogIn',
fields: [
{
label: 'Custom Content',
label: 'Identify User Persona',
name: 'user_category',
type: 'checkbox',
description:
'Enable this option to identify the user persona during signup.',
},
{
label: 'Disable signup',
name: 'disable_signup',
type: 'checkbox',
description:
'New users will have to be manually registered by Admins.',
},
{
label: 'Signup Consent HTML',
name: 'custom_signup_content',
type: 'Code',
mode: 'htmlmixed',
rows: 10,
},
{
label: 'Ask for Occupation',
name: 'user_category',
type: 'checkbox',
description:
'Enable this option to ask users to select their occupation during the signup process.',
},
],
},
],

View File

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

View File

@@ -1,7 +1,7 @@
<template>
<div v-if="quiz.data">
<div
class="bg-surface-blue-2 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-ink-blue-2"
class="bg-surface-blue-2 space-y-1 py-2 px-2 mb-4 rounded-md text-sm text-ink-blue-3"
>
<div class="leading-5">
{{

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -264,17 +264,20 @@ import {
Button,
TextEditor,
createResource,
usePageMeta,
} from 'frappe-ui'
import Link from '@/components/Controls/Link.vue'
import { useRouter } from 'vue-router'
import { showToast } from '@/utils'
import { Image } from 'lucide-vue-next'
import { capture } from '@/telemetry'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import { useOnboarding } from 'frappe-ui/frappe'
import { sessionStore } from '../stores/session'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import Link from '@/components/Controls/Link.vue'
const router = useRouter()
const user = inject('$user')
const { brand } = sessionStore()
const { updateOnboardingStep } = useOnboarding('learning')
const props = defineProps({
@@ -427,10 +430,13 @@ const createNewBatch = () => {
{},
{
onSuccess(data) {
if (user.data?.is_system_manager) {
updateOnboardingStep('create_first_batch', true, false, () => {
localStorage.setItem('firstBatch', data.name)
})
}
capture('batch_created')
updateOnboardingStep('create_first_batch', true, false, () => {
localStorage.setItem('firstBatch', data.name)
})
router.push({
name: 'BatchDetail',
params: {
@@ -505,4 +511,11 @@ const breadcrumbs = computed(() => {
})
return crumbs
})
usePageMeta(() => {
return {
title: props.batchName == 'new' ? 'New Batch' : batchDetail.data?.title,
icon: brand.favicon,
}
})
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -3,15 +3,11 @@
<div class="grid md:grid-cols-[70%,30%] h-full">
<div>
<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" />
<div class="flex items-center mt-3 md:mt-0">
<Button
v-if="courseResource.data?.name"
@click="trashCourse()"
class="invisible group-hover:visible"
>
<Button v-if="courseResource.data?.name" @click="trashCourse()">
<template #icon>
<Trash2 class="w-4 h-4 stroke-1.5" />
</template>
@@ -253,6 +249,7 @@ import {
createResource,
FormControl,
FileUploader,
usePageMeta,
} from 'frappe-ui'
import {
inject,
@@ -264,18 +261,20 @@ import {
watch,
getCurrentInstance,
} from 'vue'
import { showToast, updateDocumentTitle } from '@/utils'
import Link from '@/components/Controls/Link.vue'
import { showToast } from '@/utils'
import { Image, Trash2, X } from 'lucide-vue-next'
import { useRouter } from 'vue-router'
import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
import { capture } from '@/telemetry'
import { useOnboarding } from 'frappe-ui/frappe'
import { sessionStore } from '../stores/session'
import { useSettings } from '@/stores/settings'
import Link from '@/components/Controls/Link.vue'
import CourseOutline from '@/components/CourseOutline.vue'
import MultiSelect from '@/components/Controls/MultiSelect.vue'
const user = inject('$user')
const newTag = ref('')
const { brand } = sessionStore()
const router = useRouter()
const instructors = ref([])
const settingsStore = useSettings()
@@ -401,7 +400,7 @@ const courseResource = createResource({
'paid_course',
'featured',
'enable_certification',
'paid_certifiate',
'paid_certificate',
]
for (let idx in checkboxes) {
let key = checkboxes[idx]
@@ -444,11 +443,14 @@ const submitCourse = () => {
} else {
courseCreationResource.submit(course, {
onSuccess(data) {
if (user.data?.is_system_manager) {
updateOnboardingStep('create_first_course', true, false, () => {
localStorage.setItem('firstCourse', data.name)
})
}
capture('course_created')
showToast('Success', 'Course created successfully', 'check')
updateOnboardingStep('create_first_course', true, false, () => {
localStorage.setItem('firstCourse', data.name)
})
router.push({
name: 'CourseForm',
params: { courseName: data.name },
@@ -574,12 +576,10 @@ const breadcrumbs = computed(() => {
return crumbs
})
const pageMeta = computed(() => {
usePageMeta(() => {
return {
title: 'Create a Course',
description: 'Create or edit a course for your learning system.',
title: courseResource.data?.title || __('New Course'),
icon: brand.favicon,
}
})
updateDocumentTitle(pageMeta)
</script>

View File

@@ -4,7 +4,7 @@
>
<Breadcrumbs :items="breadcrumbs" />
<router-link
v-if="user.data?.is_moderator"
v-if="canCreateCourse()"
:to="{
name: 'CourseForm',
params: { courseName: 'new' },
@@ -100,10 +100,12 @@ import {
FormControl,
Select,
TabButtons,
usePageMeta,
} from 'frappe-ui'
import { computed, inject, onMounted, ref, watch } from 'vue'
import { BookOpen, Plus } from 'lucide-vue-next'
import { updateDocumentTitle } from '@/utils'
import { sessionStore } from '@/stores/session'
import { canCreateCourse } from '@/utils'
import CourseCard from '@/components/CourseCard.vue'
const user = inject('$user')
@@ -116,6 +118,7 @@ const title = ref('')
const certification = ref(false)
const filters = ref({})
const currentTab = ref('Live')
const { brand } = sessionStore()
onMounted(() => {
setFiltersFromQuery()
@@ -303,12 +306,10 @@ const breadcrumbs = computed(() => [
},
])
const pageMeta = computed(() => {
usePageMeta(() => {
return {
title: 'Courses',
description: 'All published courses.',
title: __('Courses'),
icon: brand.favicon,
}
})
updateDocumentTitle(pageMeta)
</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,
TextEditor,
FileUploader,
usePageMeta,
} from 'frappe-ui'
import { computed, onMounted, reactive, inject } from 'vue'
import { FileText, X } from 'lucide-vue-next'
import { sessionStore } from '@/stores/session'
import { useRouter } from 'vue-router'
import { getFileSize, showToast } from '../utils'
const user = inject('$user')
const router = useRouter()
const { brand } = sessionStore()
const props = defineProps({
jobName: {
@@ -319,4 +322,11 @@ const breadcrumbs = computed(() => {
]
return crumbs
})
usePageMeta(() => {
return {
title: props.jobName == 'new' ? 'New Job' : jobDetail.data?.title,
icon: brand.favicon,
}
})
</script>

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

View File

@@ -25,7 +25,7 @@
</router-link>
</header>
<div>
<div class="lg:w-3/4 mx-auto p-5">
<div v-if="jobs.data?.length" class="p-5">
<div
class="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:items-center justify-between mb-5"
>
@@ -58,10 +58,7 @@
</div>
</div>
<div
v-if="jobs.data?.length"
class="grid grid-cols-1 lg:grid-cols-2 gap-5"
>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-5">
<router-link
v-for="job in jobs.data"
:to="{
@@ -73,22 +70,42 @@
<JobCard :job="job" />
</router-link>
</div>
<div v-else class="text-ink-gray-7 italic p-5 w-fit mx-auto">
{{ __('No jobs posted') }}
</div>
<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>
</template>
<script setup>
import { Button, Breadcrumbs, createResource, FormControl } from 'frappe-ui'
import { Plus, Search } from 'lucide-vue-next'
import {
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 JobCard from '@/components/JobCard.vue'
import { updateDocumentTitle } from '@/utils'
const user = inject('$user')
const jobType = ref(null)
const { brand } = sessionStore()
const searchQuery = ref('')
const filters = ref({})
const orFilters = ref({})
@@ -147,12 +164,11 @@ const jobTypes = computed(() => {
{ label: __('Freelance'), value: 'Freelance' },
]
})
const pageMeta = computed(() => {
usePageMeta(() => {
return {
title: 'Jobs',
description: 'An open job board for the community',
title: __('Jobs'),
icon: brand.favicon,
}
})
updateDocumentTitle(pageMeta)
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -40,6 +40,18 @@
</Button>
</div>
</div>
<div
v-else
class="text-center p-5 text-ink-gray-5 mt-52 w-3/4 md:w-1/2 mx-auto space-y-2"
>
<BookOpen class="size-10 mx-auto stroke-1 text-ink-gray-4" />
<div class="text-xl font-medium">
{{ __('No submissions') }}
</div>
<div class="leading-5">
{{ __('No quiz submissions found. Please check again later.') }}
</div>
</div>
</template>
<script setup>
import {
@@ -51,10 +63,14 @@ import {
ListRows,
ListHeader,
ListHeaderItem,
usePageMeta,
} from 'frappe-ui'
import { BookOpen } from 'lucide-vue-next'
import { computed, onMounted, inject } from 'vue'
import { sessionStore } from '../stores/session'
import { useRouter } from 'vue-router'
const { brand } = sessionStore()
const router = useRouter()
const user = inject('$user')
@@ -105,4 +121,11 @@ const quizColumns = computed(() => {
const breadcrumbs = computed(() => {
return [{ label: __('Quiz Submissions') }]
})
usePageMeta(() => {
return {
title: __('Quiz Submissions'),
icon: brand.favicon,
}
})
</script>

View File

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

View File

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

View File

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

View File

@@ -2,10 +2,11 @@ import { defineStore } from 'pinia'
import { createResource } from 'frappe-ui'
import { usersStore } from './user'
import router from '@/router'
import { ref, computed } from 'vue'
import { computed, reactive, ref } from 'vue'
export const sessionStore = defineStore('lms-session', () => {
let { userResource } = usersStore()
const brand = reactive({})
function sessionUser() {
let cookies = new URLSearchParams(document.cookie.split('; ').join('&'))
@@ -46,7 +47,11 @@ export const sessionStore = defineStore('lms-session', () => {
cache: 'brand',
auto: true,
onSuccess(data) {
document.querySelector("link[rel='icon']").href = data.favicon
brand.name = data.app_name
brand.logo = data.app_logo
brand.favicon =
data.favicon?.file_url ||
'/assets/lms/frontend/public/learning.svg'
},
})
@@ -61,6 +66,7 @@ export const sessionStore = defineStore('lms-session', () => {
isLoggedIn,
login,
logout,
brand,
branding,
sidebarSettings,
}

View File

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

View File

@@ -14,6 +14,7 @@ import dayjs from '@/utils/dayjs'
import Embed from '@editorjs/embed'
import SimpleImage from '@editorjs/simple-image'
import Table from '@editorjs/table'
import { usersStore } from '../stores/user'
export function createToast(options) {
toast({
@@ -567,3 +568,8 @@ export const escapeHTML = (text) => {
(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: {
allowedHosts: ['fs', 'onb1'],
allowedHosts: ['fs', 'onb2'],
},
resolve: {
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",
"company_name",
"company_logo",
"company_website",
"name",
"creation",
"description",
@@ -1258,6 +1259,11 @@ def is_guest_allowed():
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()
def cancel_evaluation(evaluation):
evaluation = frappe._dict(evaluation)

View File

@@ -161,7 +161,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-11-14 13:46:56.838659",
"modified": "2025-04-10 15:19:22.400932",
"modified_by": "Administrator",
"module": "LMS",
"name": "Course Lesson",
@@ -189,14 +189,28 @@
"print": 1,
"read": 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,
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}
}

View File

@@ -11,66 +11,34 @@ import json
class CourseLesson(Document):
def validate(self):
# self.check_and_create_folder()
def on_update(self):
self.validate_quiz_id()
def validate_quiz_id(self):
if self.quiz_id and not frappe.db.exists("LMS Quiz", self.quiz_id):
frappe.throw(_("Invalid Quiz ID"))
def on_update(self):
dynamic_documents = ["Exercise", "Quiz"]
for section in dynamic_documents:
self.update_lesson_name_in_document(section)
if self.content:
self.save_lesson_details_in_quiz(self.content)
def update_lesson_name_in_document(self, section):
doctype_map = {"Exercise": "LMS Exercise", "Quiz": "LMS Quiz"}
macros = find_macros(self.body)
documents = [value for name, value in macros if name == section]
index = 1
for name in documents:
e = frappe.get_doc(doctype_map[section], name)
e.lesson = self.name
e.index_ = index
e.course = self.course
e.save(ignore_permissions=True)
index += 1
self.update_orphan_documents(doctype_map[section], documents)
if self.instructor_content:
self.save_lesson_details_in_quiz(self.instructor_content)
def update_orphan_documents(self, doctype, documents):
"""Updates the documents that were previously part of this lesson,
but not any more.
"""
linked_documents = {
row["name"] for row in frappe.get_all(doctype, {"lesson": self.name})
}
active_documents = set(documents)
orphan_documents = linked_documents - active_documents
for name in orphan_documents:
ex = frappe.get_doc(doctype, name)
ex.lesson = None
ex.course = None
ex.index_ = 0
ex.save(ignore_permissions=True)
def check_and_create_folder(self):
args = {
"doctype": "File",
"is_folder": True,
"file_name": f"{self.name} {self.course}",
}
if not frappe.db.exists(args):
folder = frappe.get_doc(args)
folder.save(ignore_permissions=True)
def get_exercises(self):
if not self.body:
return []
macros = find_macros(self.body)
exercises = [value for name, value in macros if name == "Exercise"]
return [frappe.get_doc("LMS Exercise", name) for name in exercises]
def save_lesson_details_in_quiz(self, content):
content = json.loads(self.content)
for block in content.get("blocks"):
if block.get("type") == "quiz":
quiz = block.get("data").get("quiz")
if not frappe.db.exists("LMS Quiz", quiz):
frappe.throw(_("Invalid Quiz ID in content"))
frappe.db.set_value(
"LMS Quiz",
quiz,
{
"course": self.course,
"lesson": self.name,
},
)
@frappe.whitelist()
@@ -102,7 +70,7 @@ def save_progress(lesson, course):
progress = get_course_progress(course)
capture_progress_for_analytics(progress, course)
# Had to get doc, as on_change doesn't trigger when you use set_value. The trigger is necesary for badge to get assigned.
# Had to get doc, as on_change doesn't trigger when you use set_value. The trigger is necessary for badge to get assigned.
enrollment = frappe.get_doc("LMS Enrollment", membership)
enrollment.progress = progress
enrollment.save()

View File

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

View File

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

View File

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

View File

@@ -10,6 +10,7 @@ from frappe.utils import get_url_to_list
class LMSSettings(Document):
def validate(self):
self.validate_google_settings()
self.validate_signup()
def validate_google_settings(self):
if self.send_calendar_invite_for_evaluations:
@@ -40,6 +41,10 @@ class LMSSettings(Document):
)
)
def validate_signup(self):
if self.has_value_changed("disable_signup"):
frappe.db.set_single_value("Website Settings", "disable_signup", self.disable_signup)
@frappe.whitelist()
def check_payments_app():

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

View File

@@ -7,8 +7,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Frappe LMS VERSION\n"
"Report-Msgid-Bugs-To: jannat@frappe.io\n"
"POT-Creation-Date: 2025-03-28 16:04+0000\n"
"PO-Revision-Date: 2025-03-28 16:04+0000\n"
"POT-Creation-Date: 2025-04-04 16:04+0000\n"
"PO-Revision-Date: 2025-04-04 16:04+0000\n"
"Last-Translator: jannat@frappe.io\n"
"Language-Team: jannat@frappe.io\n"
"MIME-Version: 1.0\n"

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>
<p> {{ _(" Please evaluate and grade it.") }} </p>
<br>`
<a href="/assignment-submission/{{ assignment_name }}/{{ submission_name }}">
<a href="/lms/assignment-submission/{{ assignment_name }}/{{ submission_name }}">
{{ _("Open Assignment") }}
</a>

View File

@@ -17,7 +17,7 @@
<label class="form-label sr-only"> {{ _("User Category") }} </label>
<div class="control-input-wrapper">
<div class="control-input flex align-center">
<select type="text" id="user_category" data-fieldname="user_category" style="color: var(--text-muted)"
<select type="text" id="user_category" data-fieldname="user_category" style="color: var(--text-light)"
class="input-with-feedback form-control ellipsis" data-fieldtype="Select" required>
<option value=""> {{ _("Category") }} </option>
<option value="Business Owner"> {{ _("Business Owner") }} </option>

View File

@@ -10,25 +10,58 @@ no_cache = 1
def get_context():
app_path = frappe.form_dict.get("app_path")
context = frappe._dict()
if app_path:
context.meta = get_meta(app_path)
else:
context.meta = {}
favicon = (
frappe.db.get_single_value("Website Settings", "favicon")
or "/assets/lms/frontend/favicon.png"
)
title = frappe.db.get_single_value("Website Settings", "app_name") or "Frappe Learning"
csrf_token = frappe.sessions.get_csrf_token()
frappe.db.commit() # nosemgrep
frappe.db.commit()
context = frappe._dict()
context.csrf_token = csrf_token
context.setup_complete = cint(frappe.get_system_settings("setup_complete"))
context.meta = get_meta(app_path, title, favicon)
capture("active_site", "lms")
context.favicon = frappe.db.get_single_value("Website Settings", "favicon")
context.title = title
context.favicon = favicon
return context
def get_meta(app_path):
def get_meta(app_path, title, favicon):
meta = {}
if app_path:
meta = get_meta_from_document(app_path, favicon)
route_meta = frappe.get_all("Website Meta Tag", {"parent": app_path}, ["key", "value"])
if len(route_meta) > 0:
for row in route_meta:
if row.key == "title":
meta["title"] = row.value
elif row.key == "image":
meta["image"] = row.value
elif row.key == "description":
meta["description"] = f"{meta.get('description', '')} {row.value}"
elif row.key == "keywords":
meta["keywords"] = f"{meta.get('keywords', '')} {row.value}"
elif row.key == "link":
meta["link"] = row.value
if not meta:
meta = {
"title": title,
"image": favicon,
"description": "Easy to use Learning Management System",
}
return meta
def get_meta_from_document(app_path, favicon):
if app_path == "courses":
return {
"title": _("Course List"),
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
"image": favicon,
"description": "This page lists all the courses published on our website",
"keywords": "All Courses, Courses, Learn",
"link": "/courses",
@@ -47,13 +80,18 @@ def get_meta(app_path):
course = frappe.db.get_value(
"LMS Course",
course_name,
["title", "image", "short_introduction", "tags"],
["title", "image", "description", "tags"],
as_dict=True,
)
if course.description:
soup = BeautifulSoup(course.description, "html.parser")
course.description = soup.get_text()
return {
"title": course.title,
"image": course.image,
"description": course.short_introduction,
"description": course.description,
"keywords": course.tags,
"link": f"/courses/{course_name}",
}
@@ -61,7 +99,7 @@ def get_meta(app_path):
if app_path == "batches":
return {
"title": _("Batches"),
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
"image": favicon,
"description": "This page lists all the batches published on our website",
"keywords": "All Batches, Batches, Learn",
"link": "/batches",
@@ -71,13 +109,18 @@ def get_meta(app_path):
batch = frappe.db.get_value(
"LMS Batch",
batch_name,
["title", "meta_image", "description", "category", "medium"],
["title", "meta_image", "batch_details", "category", "medium"],
as_dict=True,
)
if batch.batch_details:
soup = BeautifulSoup(batch.batch_details, "html.parser")
batch.batch_details = soup.get_text()
return {
"title": batch.title,
"image": batch.meta_image,
"description": batch.description,
"description": batch.batch_details,
"keywords": f"{batch.category} {batch.medium}",
"link": f"/batches/details/{batch_name}",
}
@@ -87,7 +130,7 @@ def get_meta(app_path):
if "new/edit" in app_path:
return {
"title": _("New Batch"),
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
"image": favicon,
"description": "Create a new batch",
"keywords": "New Batch, Create Batch",
"link": "/lms/batches/new/edit",
@@ -95,13 +138,18 @@ def get_meta(app_path):
batch = frappe.db.get_value(
"LMS Batch",
batch_name,
["title", "meta_image", "description", "category", "medium"],
["title", "meta_image", "batch_details", "category", "medium"],
as_dict=True,
)
if batch.batch_details:
soup = BeautifulSoup(batch.batch_details, "html.parser")
batch.batch_details = soup.get_text()
return {
"title": batch.title,
"image": batch.meta_image,
"description": batch.description,
"description": batch.batch_details,
"keywords": f"{batch.category} {batch.medium}",
"link": f"/batches/{batch_name}",
}
@@ -109,7 +157,7 @@ def get_meta(app_path):
if app_path == "job-openings":
return {
"title": _("Job Openings"),
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
"image": favicon,
"description": "This page lists all the job openings published on our website",
"keywords": "Job Openings, Jobs, Vacancies",
"link": "/job-openings",
@@ -120,13 +168,13 @@ def get_meta(app_path):
job_opening = frappe.db.get_value(
"Job Opportunity",
job_opening_name,
["job_title", "company_logo", "company_name"],
["job_title", "company_logo", "description"],
as_dict=True,
)
return {
"title": job_opening.job_title,
"image": job_opening.company_logo,
"description": job_opening.company_name,
"description": job_opening.description,
"keywords": "Job Openings, Jobs, Vacancies",
"link": f"/job-openings/{job_opening_name}",
}
@@ -134,7 +182,7 @@ def get_meta(app_path):
if app_path == "statistics":
return {
"title": _("Statistics"),
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
"image": favicon,
"description": "This page lists all the statistics of this platform",
"keywords": "Enrollment Count, Completion, Signups",
"link": "/statistics",
@@ -179,3 +227,64 @@ def get_meta(app_path):
"keywords": f"{badge.title}, {badge.description}",
"link": f"/badges/{badgeName}/{email}",
}
if app_path == "quizzes":
return {
"title": _("Quizzes"),
"image": favicon,
"description": _("Test your knowledge with interactive quizzes and more."),
"keywords": "Quizzes, interactive quizzes, online quizzes",
"link": "/quizzes",
}
if re.match(r"^quizzes/[^/]+$", app_path):
quiz_name = app_path.split("/")[1]
quiz = frappe.db.get_value(
"LMS Quiz",
quiz_name,
["title"],
as_dict=True,
)
if quiz:
return {
"title": quiz.title,
"image": favicon,
"description": "Test your knowledge with interactive quizzes.",
"keywords": quiz.title,
"link": f"/quizzes/{quiz_name}",
}
if app_path == "assignments":
return {
"title": _("Assignments"),
"image": favicon,
"description": _("Test your knowledge with interactive assignments and more."),
"keywords": "Assignments, interactive assignments, online assignments",
"link": "/assignments",
}
if re.match(r"^assignments/[^/]+$", app_path):
assignment_name = app_path.split("/")[1]
assignment = frappe.db.get_value(
"LMS Assignment",
assignment_name,
["title"],
as_dict=True,
)
if assignment:
return {
"title": assignment.title,
"image": favicon,
"description": "Test your knowledge with interactive assignments.",
"keywords": assignment.title,
"link": f"/assignments/{assignment_name}",
}
if app_path == "programs":
return {
"title": _("Programs"),
"image": favicon,
"description": "This page lists all the programs published on our website",
"keywords": "All Programs, Programs, Learn",
"link": "/programs",
}