Merge branch 'develop' into fix-seats-badge

This commit is contained in:
Jannat Patel
2024-06-24 14:30:24 +05:30
committed by GitHub
21 changed files with 484 additions and 96 deletions

View File

@@ -13,6 +13,6 @@ module.exports = defineConfig({
openMode: 0,
},
e2e: {
baseUrl: "http://pyp:8000",
baseUrl: "http://test_site_ui:8000",
},
});

View File

@@ -108,14 +108,11 @@ describe("Course Creation", () => {
cy.get("[id^=headlessui-disclosure-panel-").within(() => {
cy.get("div").contains("Test Lesson").click();
});
cy.wait(1000);
cy.wait(3000);
// View Lesson
cy.url().should("include", "/learn/1-1");
cy.get("div").contains("Test Lesson");
cy.get("div").contains(
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now. "
);
cy.get("video")
.should("be.visible")
@@ -123,6 +120,10 @@ describe("Course Creation", () => {
.invoke("attr", "src")
.should("include", "/files/Youtube");
cy.get("div").contains(
"This is an extremely big paragraph that is meant to test the UI. This is a very long paragraph. It contains more than once sentence. Its meant to be this long as this is a UI test. Its unbearably long and I'm not sure why I'm typing this much. I'm just going to keep typing until I feel like its long enough. I think its long enough now. I'm going to stop typing now."
);
// Add Discussion
cy.button("New Question").click();
cy.wait(500);

View File

@@ -0,0 +1,137 @@
<template>
<div>
<!-- <audio width="100%" controls controlsList="nodownload" class="mb-4">
<source :src="encodeURI(file)" type="audio/mp3" />
</audio> -->
<audio @ended="handleAudioEnd" controlsList="nodownload" class="mb-4">
<source :src="encodeURI(file)" type="audio/mp3" />
</audio>
<div
class="flex items-center space-x-2 shadow rounded-lg p-1 w-1/2 mx-auto"
>
<Button variant="ghost" @click="togglePlay">
<template #icon>
<Play v-if="!isPlaying" class="w-4 h-4 text-gray-900" />
<Pause v-else class="w-4 h-4 text-gray-900" />
</template>
</Button>
<input
type="range"
min="0"
:max="duration"
step="0.1"
v-model="currentTime"
@input="changeCurrentTime"
class="duration-slider w-full h-1"
/>
<span class="text-xs text-gray-900 font-medium">
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
</span>
<Button variant="ghost" @click="toggleMute">
<template #icon>
<Volume2 v-if="!isMuted" class="w-4 h-4 text-gray-900" />
<VolumeX v-else class="w-4 h-4 text-gray-900" />
</template>
</Button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import { Play, Pause, Volume2, VolumeX } from 'lucide-vue-next'
import { Button } from 'frappe-ui'
const isPlaying = ref(false)
const audio = ref(null)
let isMuted = ref(false)
let currentTime = ref(0)
let duration = ref(0)
const props = defineProps({
file: {
type: String,
required: true,
},
})
onMounted(() => {
setTimeout(() => {
audio.value = document.querySelector('audio')
console.log(audio.value)
audio.value.onloadedmetadata = () => {
duration.value = audio.value.duration
}
audio.value.ontimeupdate = () => {
currentTime.value = audio.value.currentTime
}
}, 0)
})
const togglePlay = () => {
if (audio.value.paused) {
audio.value.play()
isPlaying.value = true
} else {
audio.value.pause()
isPlaying.value = false
}
}
const toggleMute = () => {
audio.value.muted = !audio.value.muted
isMuted.value = audio.value.muted
}
const changeCurrentTime = () => {
audio.value.currentTime = currentTime.value
}
const handleAudioEnd = () => {
isPlaying.value = false
}
const formatTime = (time) => {
const minutes = Math.floor(time / 60)
const seconds = Math.floor(time % 60)
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
}
watch(isPlaying, (newVal) => {
if (newVal) {
audio.value.play()
} else {
audio.value.pause()
}
})
</script>
<style>
.duration-slider {
flex: 1;
-webkit-appearance: none;
appearance: none;
background-color: theme('colors.gray.400');
cursor: pointer;
}
.duration-slider::-webkit-slider-thumb {
height: 10px;
width: 10px;
-webkit-appearance: none;
background-color: theme('colors.gray.900');
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
input[type='range'] {
overflow: hidden;
width: 150px;
-webkit-appearance: none;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
cursor: pointer;
box-shadow: -150px 0 0 150px theme('colors.gray.900');
}
}
</style>

View File

@@ -36,19 +36,25 @@
:endDate="batch.end_date"
class="mb-3"
/>
<div class="flex items-center">
<div class="flex items-center mb-3">
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ formatTime(batch.start_time) }} - {{ formatTime(batch.end_time) }}
</span>
</div>
<div v-if="batch.timezone" class="flex items-center">
<Globe class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ batch.timezone }}
</span>
</div>
</div>
</div>
</template>
<script setup>
import { Badge } from 'frappe-ui'
import { formatTime } from '../utils'
import { Clock, BookOpen } from 'lucide-vue-next'
import { Clock, BookOpen, Globe } from 'lucide-vue-next'
import DateRange from '@/components/Common/DateRange.vue'
const props = defineProps({

View File

@@ -26,13 +26,19 @@
:endDate="batch.data.end_date"
class="mb-3"
/>
<div class="flex items-center">
<div class="flex items-center mb-3">
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ formatTime(batch.data.start_time) }} -
{{ formatTime(batch.data.end_time) }}
</span>
</div>
<div v-if="batch.data.timezone" class="flex items-center">
<Globe class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ batch.data.timezone }}
</span>
</div>
<router-link
v-if="user?.data?.is_moderator"
:to="{
@@ -91,7 +97,7 @@
<script setup>
import { inject, computed } from 'vue'
import { Badge, Button } from 'frappe-ui'
import { BookOpen, Clock } from 'lucide-vue-next'
import { BookOpen, Clock, Globe } from 'lucide-vue-next'
import { formatNumberIntoCurrency, formatTime } from '@/utils'
import DateRange from '@/components/Common/DateRange.vue'

View File

@@ -0,0 +1,178 @@
<template>
<div ref="videoContainer" class="video-block group relative">
<video @timeupdate="updateTime" @ended="videoEnded" class="rounded-lg">
<source :src="fileURL" :type="type" />
</video>
<div
class="flex items-center space-x-2 bg-gray-200 rounded-lg p-0.5 absolute bottom-3 w-[98%] left-0 right-0 mx-auto"
>
<Button variant="ghost">
<template #icon>
<Play
v-if="!playing"
@click="playVideo"
class="w-4 h-4 text-gray-900"
/>
<Pause v-else @click="pauseVideo" class="w-4 h-4 text-gray-900" />
</template>
</Button>
<Button variant="ghost" @click="toggleMute">
<template #icon>
<Volume2 v-if="!muted" class="w-4 h-4 text-gray-900" />
<VolumeX v-else class="w-4 h-4 text-gray-900" />
</template>
</Button>
<input
type="range"
min="0"
:max="duration"
step="0.1"
v-model="currentTime"
@input="changeCurrentTime"
class="duration-slider w-full h-1"
/>
<span class="text-xs font-medium">
{{ formatTime(currentTime) }} / {{ formatTime(duration) }}
</span>
<Button variant="ghost" @click="toggleFullscreen">
<template #icon>
<Maximize class="w-4 h-4 text-gray-900" />
</template>
</Button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, computed } from 'vue'
import { Play, Pause, Maximize, Volume2, VolumeX } from 'lucide-vue-next'
import { Button } from 'frappe-ui'
const videoRef = ref(null)
const videoContainer = ref(null)
let playing = ref(false)
let currentTime = ref(0)
let duration = ref(0)
let muted = ref(false)
const props = defineProps({
file: {
type: String,
required: true,
},
type: {
type: String,
default: 'video/mp4',
},
})
onMounted(() => {
setTimeout(() => {
videoRef.value = document.querySelector('video')
videoRef.value.onloadedmetadata = () => {
duration.value = videoRef.value.duration
}
videoRef.value.ontimeupdate = () => {
currentTime.value = videoRef.value.currentTime
}
}, 0)
})
const fileURL = computed(() => {
if (isYoutube) {
let url = props.file
if (url.includes('watch?v=')) {
url = url.replace('watch?v=', 'embed/')
}
return `${url}?autoplay=0&controls=0&disablekb=1&playsinline=1&cc_load_policy=1&cc_lang_pref=auto`
}
return props.file
})
const isYoutube = computed(() => {
return props.type == 'video/youtube'
})
const playVideo = () => {
videoRef.value.play()
playing.value = true
}
const pauseVideo = () => {
videoRef.value.pause()
playing.value = false
}
const videoEnded = () => {
playing.value = false
}
const toggleMute = () => {
videoRef.value.muted = !videoRef.value.muted
muted.value = videoRef.value.muted
}
const changeCurrentTime = () => {
videoRef.value.currentTime = currentTime.value
}
const formatTime = (time) => {
const minutes = Math.floor(time / 60)
const seconds = Math.floor(time % 60)
return `${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
}
const toggleFullscreen = () => {
if (document.fullscreenElement) {
document.exitFullscreen()
} else {
videoContainer.value.requestFullscreen()
}
}
</script>
<style scoped>
.video-block {
width: 100%;
max-width: 900px;
margin: 0 auto;
}
.video-block video {
width: 100%;
height: auto;
}
iframe {
width: 100%;
min-height: 500px;
}
.duration-slider {
flex: 1;
-webkit-appearance: none;
appearance: none;
background-color: theme('colors.gray.400');
cursor: pointer;
}
.duration-slider::-webkit-slider-thumb {
height: 10px;
width: 10px;
-webkit-appearance: none;
background-color: theme('colors.gray.900');
}
@media screen and (-webkit-min-device-pixel-ratio: 0) {
input[type='range'] {
overflow: hidden;
width: 150px;
-webkit-appearance: none;
}
input[type='range']::-webkit-slider-thumb {
-webkit-appearance: none;
cursor: pointer;
box-shadow: -500px 0 0 500px theme('colors.gray.900');
}
}
</style>

View File

@@ -87,13 +87,19 @@
:endDate="batch.data.end_date"
class="mb-3"
/>
<div class="flex items-center mb-6">
<div class="flex items-center mb-4">
<Clock class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ formatTime(batch.data.start_time) }} -
{{ formatTime(batch.data.end_time) }}
</span>
</div>
<div v-if="batch.data.timezone" class="flex items-center mb-4">
<Globe class="h-4 w-4 stroke-1.5 mr-2 text-gray-700" />
<span>
{{ batch.data.timezone }}
</span>
</div>
</div>
<AnnouncementModal
v-model="showAnnouncementModal"
@@ -159,6 +165,7 @@ import {
Mail,
SendIcon,
MessageCircle,
Globe,
} from 'lucide-vue-next'
import { formatTime } from '@/utils'
import BatchDashboard from '@/components/BatchDashboard.vue'

View File

@@ -9,30 +9,24 @@
</Button>
</header>
<div class="py-5">
<div class="container">
<div class="container border-b">
<div class="text-lg font-semibold mb-4">
{{ __('Details') }}
</div>
<div class="grid grid-cols-2 gap-10">
<div class="grid grid-cols-2 gap-10 mb-4">
<div>
<FormControl
v-model="batch.title"
:label="__('Title')"
class="mb-4"
/>
<FormControl
v-model="batch.description"
:label="__('Description')"
type="textarea"
class="mb-4"
/>
</div>
<div>
<FormControl
v-model="batch.published"
type="checkbox"
:label="__('Published')"
/>
</div>
<div class="flex flex-col">
<FileUploader
v-if="!batch.image"
class="mt-4"
@@ -52,7 +46,7 @@
</div>
</template>
</FileUploader>
<div v-else class="mt-4">
<div v-else class="my-4">
<div class="text-xs text-gray-600 mb-1">
{{ __('Meta Image') }}
</div>
@@ -74,10 +68,21 @@
/>
</div>
</div>
<FormControl
v-model="batch.allow_self_enrollment"
type="checkbox"
:label="__('Allow self enrollment')"
/>
</div>
</div>
</div>
<div class="container border-b mb-5">
<div class="container border-b mb-4">
<FormControl
v-model="batch.description"
:label="__('Description')"
type="textarea"
class="my-4"
/>
<div>
<label class="block text-sm text-gray-600 mb-1">
{{ __('Batch Details') }}
@@ -91,9 +96,9 @@
/>
</div>
</div>
<div class="container border-b mb-5">
<div class="container border-b mb-4">
<div class="text-lg font-semibold mb-4">
{{ __('Settings') }}
{{ __('Date and Time') }}
</div>
<div class="grid grid-cols-2 gap-10">
<div>
@@ -109,6 +114,8 @@
type="date"
class="mb-4"
/>
</div>
<div>
<FormControl
v-model="batch.start_time"
:label="__('Start Time')"
@@ -121,7 +128,20 @@
type="time"
class="mb-4"
/>
<FormControl
v-model="batch.timezone"
:label="__('Timezone')"
type="text"
class="mb-4"
/>
</div>
</div>
</div>
<div class="container border-b mb-4">
<div class="text-lg font-semibold mb-4">
{{ __('Settings') }}
</div>
<div class="grid grid-cols-2 gap-10">
<div>
<FormControl
v-model="batch.seat_count"
@@ -135,6 +155,8 @@
type="date"
class="mb-4"
/>
</div>
<div>
<FormControl
v-model="batch.medium"
type="select"
@@ -188,7 +210,7 @@
</div>
</template>
<script setup>
import { computed, onMounted, inject, reactive } from 'vue'
import { computed, onMounted, inject, reactive, onBeforeUnmount } from 'vue'
import {
Breadcrumbs,
FormControl,
@@ -221,10 +243,12 @@ const batch = reactive({
end_date: '',
start_time: '',
end_time: '',
timezone: '',
evaluation_end_date: '',
seat_count: '',
medium: '',
category: '',
allow_self_enrollment: false,
image: null,
paid_batch: false,
currency: '',
@@ -236,6 +260,22 @@ onMounted(() => {
if (props.batchName != 'new') {
batchDetail.reload()
}
window.addEventListener('keydown', keyboardShortcut)
})
const keyboardShortcut = (e) => {
if (
e.key === 's' &&
(e.ctrlKey || e.metaKey) &&
!e.target.classList.contains('ProseMirror')
) {
saveBatch()
e.preventDefault()
}
}
onBeforeUnmount(() => {
window.removeEventListener('keydown', keyboardShortcut)
})
const newBatch = createResource({

View File

@@ -15,6 +15,7 @@ export const usersStore = defineStore('lms-users', () => {
const allUsers = createResource({
url: 'lms.lms.api.get_all_users',
cache: ['allUsers'],
auto: true,
})
return {

View File

@@ -0,0 +1,32 @@
import Embed from '@editorjs/embed'
import VideoBlock from '@/components/VideoBlock.vue'
import { createApp } from 'vue'
export class CustomEmbed extends Embed {
render() {
const container = super.render()
const { service, source, embed } = this.data
if (service === 'youtube' || service === 'vimeo') {
// Remove the iframe or existing embed content
container.innerHTML = ''
// Create a placeholder element for Vue component
const vueContainer = document.createElement('div')
vueContainer.setAttribute('data-service', service)
vueContainer.setAttribute('data-video-id', this.data.source)
// Append the Vue placeholder
container.appendChild(vueContainer)
console.log(source)
// Mount the Vue component (using a global Vue instance)
const app = createApp(VideoBlock, {
file: source,
type: 'video/youtube',
})
app.mount(vueContainer)
}
return container
}
}

View File

@@ -4,12 +4,12 @@ import { Quiz } from '@/utils/quiz'
import { Upload } from '@/utils/upload'
import Header from '@editorjs/header'
import Paragraph from '@editorjs/paragraph'
import Embed from '@editorjs/embed'
import { CodeBox } from '@/utils/code'
import NestedList from '@editorjs/nested-list'
import InlineCode from '@editorjs/inline-code'
import { watch } from 'vue'
import { watch, createApp } from 'vue'
import dayjs from '@/utils/dayjs'
import Embed from '@editorjs/embed'
export function createToast(options) {
toast({
@@ -167,6 +167,7 @@ export function getEditorTools() {
youtube: true,
vimeo: true,
codepen: true,
aparat: true,
slides: {
regex: /https:\/\/docs\.google\.com\/presentation\/d\/e\/([A-Za-z0-9_-]+)\/pub/,
embedUrl:

View File

@@ -1,3 +1,7 @@
import AudioBlock from '@/components/AudioBlock.vue'
import VideoBlock from '@/components/VideoBlock.vue'
import { createApp } from 'vue'
export class Upload {
constructor({ data, api, readOnly }) {
this.data = data
@@ -10,19 +14,23 @@ export class Upload {
render() {
this.wrapper = document.createElement('div')
this.wrapper.innerHTML = this.renderUpload(this.data)
this.renderUpload(this.data)
return this.wrapper
}
renderUpload(file) {
if (this.isVideo(file.file_type)) {
return `<video controls width='100%' controlsList='nodownload' class="mb-4" oncontextmenu="return false;">
<source src=${encodeURI(file.file_url)} type='video/mp4'>
</video>`
const app = createApp(VideoBlock, {
file: file.file_url,
})
app.mount(this.wrapper)
return
} else if (this.isAudio(file.file_type)) {
return `<audio controls width='100%' controls controlsList='nodownload' class="mb-4">
<source src=${encodeURI(file.file_url)} type='audio/mp3'>
</audio>`
const app = createApp(AudioBlock, {
file: file.file_url,
})
app.mount(this.wrapper)
return
} else if (file.file_type == 'pdf') {
return `<iframe src="${encodeURI(
file.file_url

View File

@@ -9,14 +9,15 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"published",
"title",
"start_date",
"end_date",
"column_break_4",
"allow_self_enrollment",
"start_time",
"end_time",
"published",
"allow_self_enrollment",
"timezone",
"section_break_rgfj",
"medium",
"category",
@@ -301,11 +302,17 @@
"fieldname": "allow_self_enrollment",
"fieldtype": "Check",
"label": "Allow Self Enrollment"
},
{
"fieldname": "timezone",
"fieldtype": "Data",
"label": "Timezone",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-05-14 14:47:48.839162",
"modified": "2024-06-21 11:49:32.582832",
"modified_by": "Administrator",
"module": "LMS",
"name": "LMS Batch",

View File

@@ -1,5 +1,7 @@
{% set title = frappe.db.get_value("LMS Course", doc.course, "title") %}
{% set timezone = frappe.db.get_value("LMS Batch", doc.batch, "timezone") %}
{% set timezone = timezone if timezone else '' %}
<p> {{ _("Hey {0}").format(doc.member_name) }} </p>
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short")) }}</p>
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short"), timezone) }}</p>
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>

View File

@@ -10,9 +10,9 @@
"event": "New",
"idx": 0,
"is_standard": 1,
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n\n<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n{% set timezone = frappe.db.get_value(\"LMS Batch\", doc.batch, \"timezone\") %}\n{% set timezone = timezone if timezone else '' %}\n\n<p> {{ _(\"Hey {0}\").format(doc.member_name) }} </p>\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\"), timezone) }}</p>\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
"message_type": "HTML",
"modified": "2023-11-29 17:34:54.514031",
"modified": "2024-06-21 13:15:38.588768",
"modified_by": "Administrator",
"module": "LMS",
"name": "Certificate Request Creation",
@@ -23,6 +23,9 @@
},
{
"receiver_by_document_field": "evaluator"
},
{
"receiver_by_role": "Frappe School Admin"
}
],
"send_system_notification": 0,

View File

@@ -1,5 +1,7 @@
{% set title = frappe.db.get_value("LMS Course", doc.course, "title") %}
{% set timezone = frappe.db.get_value("LMS Batch", doc.batch, "timezone") %}
{% set timezone = timezone if timezone else '' %}
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short")) }}</p>
<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(title, frappe.utils.format_date(doc.date, "medium"), frappe.utils.format_time(doc.start_time, "short"), timezone) }}</p>
<p> {{ _("Please prepare well and be on time for the evaluations.") }} </p>

View File

@@ -11,9 +11,9 @@
"event": "Days Before",
"idx": 0,
"is_standard": 1,
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\")) }}</p>\n\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
"message": "{% set title = frappe.db.get_value(\"LMS Course\", doc.course, \"title\") %}\n{% set timezone = frappe.db.get_value(\"LMS Batch\", doc.batch, \"timezone\") %}\n{% set timezone = timezone if timezone else '' %}\n\n<p> {{ _('Your evaluation for the course {0} has been scheduled on {1} at {2} {3}.').format(title, frappe.utils.format_date(doc.date, \"medium\"), frappe.utils.format_time(doc.start_time, \"short\"), timezone) }}</p>\n\n<p> {{ _(\"Please prepare well and be on time for the evaluations.\") }} </p>\n",
"message_type": "HTML",
"modified": "2023-11-29 17:26:53.355501",
"modified": "2024-06-21 13:17:26.360030",
"modified_by": "Administrator",
"module": "LMS",
"name": "Certificate Request Reminder",
@@ -29,4 +29,4 @@
"send_system_notification": 0,
"send_to_all_assignees": 0,
"subject": "Reminder for Certificate Evaluation"
}
}

View File

@@ -1512,6 +1512,7 @@ def get_batch_details(batch):
"paid_batch",
"evaluation_end_date",
"allow_self_enrollment",
"timezone",
],
as_dict=True,
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 440 B

View File

@@ -1,52 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="/assets/lms/frontend/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Frappe Learning</title>
<meta name="title" content="{{ meta.title }}" />
<meta name="image" content="{{ meta.image }}" />
<meta name="description" content="{{ meta.description }}" />
<meta name="keywords" content="{{ meta.keywords }}" />
<meta property="og:title" content="{{ meta.title }}" />
<meta property="og:image" content="{{ meta.image }}" />
<meta property="og:description" content="{{ meta.description }}" />
<meta name="twitter:title" content="{{ meta.title }}" />
<meta name="twitter:image" content="{{ meta.image }}" />
<meta name="twitter:description" content="{{ meta.description }}" />
<script type="module" crossorigin src="/assets/lms/frontend/assets/index-kSywU9PY.js"></script>
<link rel="modulepreload" crossorigin href="/assets/lms/frontend/assets/frappe-ui-vM9kBbGH.js">
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/frappe-ui-DzKBfka9.css">
<link rel="stylesheet" crossorigin href="/assets/lms/frontend/assets/index-CxRhs9Fi.css">
</head>
<body>
<div id="app">
<div id="seo-content">
<h1>{{ meta.title }}</h1>
<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>
<div id="modals"></div>
<div id="popovers"></div>
<script>
window.csrf_token = '{{ csrf_token }}'
document.getElementById('seo-content').style.display = 'none';
</script>
</body>
</html>

View File

@@ -82,6 +82,14 @@ def get_meta(app_path):
if re.match(r"^batches/.*$", app_path):
batch_name = app_path.split("/")[1]
if "new/edit" in app_path:
return {
"title": _("New Batch"),
"image": frappe.db.get_single_value("Website Settings", "banner_image"),
"description": "Create a new batch",
"keywords": "New Batch, Create Batch",
"link": "/lms/batches/new/edit",
}
batch = frappe.db.get_value(
"LMS Batch",
batch_name,