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
+13 -2
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")
+4 -1
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>
+9 -5
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>
+14 -1
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',
},
], ],
}, },
], ],
+3 -1
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
} }
}) })
+13 -4
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>
+6 -3
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
+1 -1
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"
+21 -9
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(\?.+)?/,
+21 -2
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",
+199 -175
View File
File diff suppressed because it is too large Load Diff
+202 -178
View File
File diff suppressed because it is too large Load Diff
+201 -177
View File
File diff suppressed because it is too large Load Diff
+202 -178
View File
File diff suppressed because it is too large Load Diff
+201 -177
View File
File diff suppressed because it is too large Load Diff
+199 -175
View File
File diff suppressed because it is too large Load Diff
+199 -175
View File
File diff suppressed because it is too large Load Diff
+199 -175
View File
File diff suppressed because it is too large Load Diff
+199 -175
View File
File diff suppressed because it is too large Load Diff
+199 -175
View File
File diff suppressed because it is too large Load Diff
+199 -175
View File
File diff suppressed because it is too large Load Diff
+199 -175
View File
File diff suppressed because it is too large Load Diff
+199 -175
View File
File diff suppressed because it is too large Load Diff
+199 -175
View File
File diff suppressed because it is too large Load Diff
+199 -175
View File
File diff suppressed because it is too large Load Diff
+202 -178
View File
File diff suppressed because it is too large Load Diff
+200 -176
View File
File diff suppressed because it is too large Load Diff
+201 -177
View File
File diff suppressed because it is too large Load Diff
+231 -207
View File
File diff suppressed because it is too large Load Diff
+8 -6
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 = {