Merge branch 'develop' of https://github.com/frappe/lms into user-persona

This commit is contained in:
Jannat Patel
2025-04-21 18:07:12 +05:30
30 changed files with 3942 additions and 3408 deletions

View File

@@ -19,12 +19,23 @@ describe("Course Creation", () => {
); );
cy.fixture("profile.png", "base64").then((fileContent) => { cy.fixture("profile.png", "base64").then((fileContent) => {
cy.get('input[type="file"]').attachFile({ /* cy.get('input[type="file"]').should("be.hidden").attachFile({
fileContent, fileContent,
fileName: "profile.png", fileName: "profile.png",
mimeType: "image/png", mimeType: "image/png",
encoding: "base64", encoding: "base64",
}); }); */
cy.get("div")
.contains("Course Image")
.siblings("div")
.children('input[type="file"]')
.attachFile({
fileContent,
fileName: "profile.png",
mimeType: "image/png",
encoding: "base64",
});
}); });
cy.get("label") cy.get("label")

View File

@@ -24,7 +24,10 @@
> >
{{ formatNumberIntoCurrency(batch.data.amount, batch.data.currency) }} {{ formatNumberIntoCurrency(batch.data.amount, batch.data.currency) }}
</div> </div>
<div class="flex items-center mb-3 text-ink-gray-7"> <div
v-if="batch.data.courses.length"
class="flex items-center mb-3 text-ink-gray-7"
>
<BookOpen class="h-4 w-4 stroke-1.5 mr-2" /> <BookOpen class="h-4 w-4 stroke-1.5 mr-2" />
<span> {{ batch.data.courses.length }} {{ __('Courses') }} </span> <span> {{ batch.data.courses.length }} {{ __('Courses') }} </span>
</div> </div>

View File

@@ -9,16 +9,20 @@
:class="{ 'default-image': !course.image }" :class="{ 'default-image': !course.image }"
:style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }" :style="{ backgroundImage: 'url(\'' + encodeURI(course.image) + '\')' }"
> >
<div <div class="flex items-center flex-wrap relative top-4 px-2 w-fit">
class="flex items-center flex-wrap space-x-1 relative top-4 px-2 w-fit" <Badge
> v-if="course.featured"
<Badge v-if="course.featured" variant="subtle" theme="green" size="md"> variant="subtle"
theme="green"
size="md"
class="mb-1 mr-1"
>
{{ __('Featured') }} {{ __('Featured') }}
</Badge> </Badge>
<div <div
v-if="course.tags" v-if="course.tags"
v-for="tag in course.tags?.split(', ')" v-for="tag in course.tags?.split(', ')"
class="text-xs bg-white text-gray-800 px-2 py-0.5 rounded-md" class="text-xs bg-white text-gray-800 px-2 py-0.5 rounded-md mb-1 mr-1"
> >
{{ tag }} {{ tag }}
</div> </div>

View File

@@ -352,10 +352,23 @@ const tabsStructure = computed(() => {
label: 'Meta Description', label: 'Meta Description',
name: 'meta_description', name: 'meta_description',
type: 'textarea', type: 'textarea',
rows: 5, rows: 4,
description: description:
"This description will be shown on lists and pages that don't have meta description", "This description will be shown on lists and pages that don't have meta description",
}, },
{
label: 'Meta Keywords',
name: 'meta_keywords',
type: 'textarea',
rows: 4,
description:
'Keywords for search engines to find your website. Separated by commas.',
},
{
label: 'Meta Image',
name: 'meta_image',
type: 'Upload',
},
], ],
}, },
], ],

View File

@@ -51,7 +51,9 @@ const props = defineProps({
const update = () => { const update = () => {
props.fields.forEach((f) => { props.fields.forEach((f) => {
if (f.type != 'Column Break') { if (f.type == 'Upload') {
props.data.doc[f.name] = f.value ? f.value.file_url : null
} else if (f.type != 'Column Break') {
props.data.doc[f.name] = f.value props.data.doc[f.name] = f.value
} }
}) })

View File

@@ -54,15 +54,24 @@
<div v-else> <div v-else>
<div class="flex items-center text-sm space-x-2"> <div class="flex items-center text-sm space-x-2">
<div <div
class="flex items-center justify-center rounded border border-outline-gray-modals bg-white w-[10rem] py-5" class="flex items-center justify-center rounded border border-outline-gray-modals bg-white w-[10rem] py-2"
> >
<img :src="data[field.name]?.file_url" class="h-6 rounded" /> <img
:src="data[field.name]?.file_url || data[field.name]"
class="w-[80%] rounded"
/>
</div> </div>
<div class="flex flex-col flex-wrap"> <div class="flex flex-col flex-wrap">
<span class="break-all text-ink-gray-9"> <span class="break-all text-ink-gray-9">
{{ data[field.name]?.file_name }} {{
data[field.name]?.file_name ||
data[field.name].split('/').pop()
}}
</span> </span>
<span class="text-sm text-ink-gray-5 mt-1"> <span
v-if="data[field.name]?.file_size"
class="text-sm text-ink-gray-5 mt-1"
>
{{ getFileSize(data[field.name]?.file_size) }} {{ getFileSize(data[field.name]?.file_size) }}
</span> </span>
</div> </div>

View File

@@ -14,13 +14,16 @@
{{ batch.data.description }} {{ batch.data.description }}
</div> </div>
<div <div
class="flex flex-col gap-2 lg:gap-0 lg:flex-row lg:items-center justify-between lg:w-1/2" class="flex flex-col gap-2 lg:gap-0 lg:flex-row lg:items-center space-x-5 lg:w-1/2"
> >
<div class="flex items-center text-ink-gray-7"> <div
v-if="batch.data?.courses?.length"
class="flex items-center text-ink-gray-7"
>
<BookOpen class="h-4 w-4 mr-2" /> <BookOpen class="h-4 w-4 mr-2" />
<span> {{ batch.data?.courses?.length }} {{ __('Courses') }} </span> <span> {{ batch.data?.courses?.length }} {{ __('Courses') }} </span>
</div> </div>
<span class="hidden lg:block" v-if="batch.data.courses" <span v-if="batch.data?.courses?.length" class="hidden lg:block"
>&middot;</span >&middot;</span
> >
<DateRange <DateRange

View File

@@ -56,7 +56,7 @@
<CourseInstructors :instructors="course.data.instructors" /> <CourseInstructors :instructors="course.data.instructors" />
</div> </div>
</div> </div>
<div v-if="course.data.tags" class="flex mt-4 w-fit"> <div v-if="course.data.tags" class="flex my-4 w-fit">
<Badge <Badge
theme="gray" theme="gray"
size="lg" size="lg"

View File

@@ -201,9 +201,9 @@ export function getEditorTools() {
regex: /(?:https?:\/\/)?(?:www\.)?(?:(?:youtu\.be\/)|(?:youtube\.com)\/(?:v\/|u\/\w\/|embed\/|watch))(?:(?:\?v=)?([^#&?=]*))?((?:[?&]\w*=\w*)*)/, regex: /(?:https?:\/\/)?(?:www\.)?(?:(?:youtu\.be\/)|(?:youtube\.com)\/(?:v\/|u\/\w\/|embed\/|watch))(?:(?:\?v=)?([^#&?=]*))?((?:[?&]\w*=\w*)*)/,
embedUrl: embedUrl:
'https://www.youtube.com/embed/<%= remote_id %>', 'https://www.youtube.com/embed/<%= remote_id %>',
html: '<iframe style="width:100%; height: 30rem;" frameborder="0" allowfullscreen></iframe>', html: `<iframe style="width:100%; height: ${
height: 320, window.innerWidth < 640 ? '15rem' : '30rem'
width: 580, };" frameborder="0" allowfullscreen></iframe>`,
id: ([id, params]) => { id: ([id, params]) => {
if (!params && id) { if (!params && id) {
return id return id
@@ -249,28 +249,40 @@ export function getEditorTools() {
return id + '?' + newParams.join('&') return id + '?' + newParams.join('&')
}, },
}, },
vimeo: true, vimeo: {
regex: /(?:http[s]?:\/\/)?(?:www\.)?vimeo\.com\/(\d+)/,
embedUrl:
'https://player.vimeo.com/video/<%= remote_id %>',
html: `<iframe style="width:100%; height: ${
window.innerWidth < 640 ? '15rem' : '30rem'
};" frameborder="0" allowfullscreen></iframe>`,
id: ([id]) => id,
},
codepen: true, codepen: true,
aparat: { aparat: {
regex: /(?:http[s]?:\/\/)?(?:www.)?aparat\.com\/v\/([^\/\?\&]+)\/?/, regex: /(?:http[s]?:\/\/)?(?:www.)?aparat\.com\/v\/([^\/\?\&]+)\/?/,
embedUrl: embedUrl:
'https://www.aparat.com/video/video/embed/videohash/<%= remote_id %>/vt/frame', 'https://www.aparat.com/video/video/embed/videohash/<%= remote_id %>/vt/frame',
html: '<iframe style="margin: 0 auto; width: 100%; height: 25rem;" frameborder="0" scrolling="no" allowtransparency="true"></iframe>', html: `<iframe style="margin: 0 auto; width: 100%; height: ${
height: 300, window.innerWidth < 640 ? '15rem' : '30rem'
width: 600, };" frameborder="0" scrolling="no" allowtransparency="true"></iframe>`,
}, },
github: true, github: true,
slides: { slides: {
regex: /https:\/\/docs\.google\.com\/presentation\/d\/([A-Za-z0-9_-]+)\/pub/, regex: /https:\/\/docs\.google\.com\/presentation\/d\/([A-Za-z0-9_-]+)\/pub/,
embedUrl: embedUrl:
'https://docs.google.com/presentation/d/<%= remote_id %>/embed', 'https://docs.google.com/presentation/d/<%= remote_id %>/embed',
html: "<iframe style='width: 100%; height: 30rem; border: 1px solid #D3D3D3; border-radius: 12px; margin: 1rem 0' frameborder='0' allowfullscreen='true'></iframe>", html: `<iframe style='width: 100%; height: ${
window.innerWidth < 640 ? '15rem' : '30rem'
}; border: 1px solid #D3D3D3; border-radius: 12px; margin: 1rem 0' frameborder='0' allowfullscreen='true'></iframe>`,
}, },
drive: { drive: {
regex: /https:\/\/drive\.google\.com\/file\/d\/([A-Za-z0-9_-]+)\/view(\?.+)?/, regex: /https:\/\/drive\.google\.com\/file\/d\/([A-Za-z0-9_-]+)\/view(\?.+)?/,
embedUrl: embedUrl:
'https://drive.google.com/file/d/<%= remote_id %>/preview', 'https://drive.google.com/file/d/<%= remote_id %>/preview',
html: "<iframe style='width: 100%; height: 25rem; border: 1px solid #D3D3D3; border-radius: 12px;' frameborder='0' allowfullscreen='true'></iframe>", html: `<iframe style='width: 100%; height: ${
window.innerWidth < 640 ? '15rem' : '30rem'
}; border: 1px solid #D3D3D3; border-radius: 12px;' frameborder='0' allowfullscreen='true'></iframe>`,
}, },
docsPublic: { docsPublic: {
regex: /https:\/\/docs\.google\.com\/document\/d\/([A-Za-z0-9_-]+)\/edit(\?.+)?/, regex: /https:\/\/docs\.google\.com\/document\/d\/([A-Za-z0-9_-]+)\/edit(\?.+)?/,

View File

@@ -60,7 +60,10 @@
"column_break_uwsp", "column_break_uwsp",
"payment_reminder_template", "payment_reminder_template",
"seo_tab", "seo_tab",
"meta_description" "meta_description",
"meta_image",
"column_break_xijv",
"meta_keywords"
], ],
"fields": [ "fields": [
{ {
@@ -370,13 +373,29 @@
"fieldname": "meta_description", "fieldname": "meta_description",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Meta Description" "label": "Meta Description"
},
{
"description": "This image will be shown on lists and pages that don't have an image by default",
"fieldname": "meta_image",
"fieldtype": "Attach Image",
"label": "Meta Image"
},
{
"description": "Common keywords that will be used for all pages",
"fieldname": "meta_keywords",
"fieldtype": "Small Text",
"label": "Meta Keywords"
},
{
"fieldname": "column_break_xijv",
"fieldtype": "Column Break"
} }
], ],
"grid_page_length": 50, "grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2025-04-17 21:58:30.365876", "modified": "2025-04-19 12:19:24.037931",
"modified_by": "sayali@frappe.io", "modified_by": "sayali@frappe.io",
"module": "LMS", "module": "LMS",
"name": "LMS Settings", "name": "LMS Settings",

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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

@@ -14,25 +14,28 @@ def get_context():
or "/assets/lms/frontend/favicon.png" or "/assets/lms/frontend/favicon.png"
) )
title = frappe.db.get_single_value("Website Settings", "app_name") or "Frappe Learning" title = frappe.db.get_single_value("Website Settings", "app_name") or "Frappe Learning"
description = frappe.db.get_single_value("LMS Settings", "meta_description")
csrf_token = frappe.sessions.get_csrf_token() csrf_token = frappe.sessions.get_csrf_token()
frappe.db.commit() frappe.db.commit()
context = frappe._dict() context = frappe._dict()
context.csrf_token = csrf_token context.csrf_token = csrf_token
context.meta = get_meta(app_path, title, favicon, description) context.meta = get_meta(app_path, title, favicon)
capture("active_site", "lms") capture("active_site", "lms")
context.title = title context.title = title
context.favicon = favicon context.favicon = favicon
return context return context
def get_meta(app_path, title, favicon, description): def get_meta(app_path, title, favicon):
meta = frappe._dict() meta = frappe._dict()
if app_path: if app_path:
meta = get_meta_from_document(app_path) meta = get_meta_from_document(app_path)
route_meta = frappe.get_all("Website Meta Tag", {"parent": app_path}, ["key", "value"]) route_meta = frappe.get_all("Website Meta Tag", {"parent": app_path}, ["key", "value"])
description = frappe.db.get_single_value("LMS Settings", "meta_description")
image = frappe.db.get_single_value("LMS Settings", "meta_image")
keywords = frappe.db.get_single_value("LMS Settings", "meta_keywords")
if len(route_meta) > 0: if len(route_meta) > 0:
for row in route_meta: for row in route_meta:
@@ -54,10 +57,9 @@ def get_meta(app_path, title, favicon, description):
meta["description"] = description meta["description"] = description
if not meta.get("image"): if not meta.get("image"):
meta["image"] = favicon meta["image"] = image or favicon
if not meta.get("keywords"): meta["keywords"] = f"{meta.get('keywords')}, {keywords}"
meta["keywords"] = ""
if not meta: if not meta:
meta = { meta = {