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", + }