diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index 018ee5e7..b0ecb717 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -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
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index da26c807..5338837c 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -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
diff --git a/commitlint.config.js b/commitlint.config.js
new file mode 100644
index 00000000..0c582f54
--- /dev/null
+++ b/commitlint.config.js
@@ -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
+ ],
+ ],
+ },
+};
diff --git a/frontend/index.html b/frontend/index.html
index 86a3dece..f5dd2162 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2,9 +2,9 @@
-
+
- Frappe Learning
+ {{ title }}
@@ -23,17 +23,6 @@
{{ meta.description }}
-
- The content here is just for seo purposes. The actual content will be loaded in a few seconds.
-
-
- 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.
-
Know More
@@ -41,8 +30,6 @@
diff --git a/frontend/src/components/UserDropdown.vue b/frontend/src/components/UserDropdown.vue
index 7a326682..072bd942 100644
--- a/frontend/src/components/UserDropdown.vue
+++ b/frontend/src/components/UserDropdown.vue
@@ -36,7 +36,7 @@
Learning
{{ convertToTitleCase(userResource.data?.full_name) }}
diff --git a/lms/www/lms.py b/lms/www/lms.py
index 67a9f235..2e36dd9c 100644
--- a/lms/www/lms.py
+++ b/lms/www/lms.py
@@ -10,25 +10,55 @@ no_cache = 1
def get_context():
app_path = frappe.form_dict.get("app_path")
+ 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"
+
context = frappe._dict()
- if app_path:
- context.meta = get_meta(app_path)
- else:
- context.meta = {}
- csrf_token = frappe.sessions.get_csrf_token()
- frappe.db.commit() # nosemgrep
- 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 +77,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 +96,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 +106,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 +127,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 +135,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 +154,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 +165,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 +179,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 +224,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",
+ }