diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 588b8b753a..380eb49518 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,10 +1,5 @@ name: Server Tests on: - push: - branches: - - main - - develop - - main-hotfix pull_request: {} permissions: @@ -97,22 +92,3 @@ jobs: uses: actions/upload-artifact@v4 with: path: /home/runner/frappe-bench/sites/coverage.xml - - coverage: - name: Coverage Wrap Up - needs: tests - runs-on: ubuntu-latest - steps: - - name: Clone - uses: actions/checkout@v5 - - - name: Download artifacts - uses: actions/download-artifact@v4 - - - name: Upload coverage data - uses: codecov/codecov-action@v5 - with: - name: Server - token: ${{ secrets.CODECOV_TOKEN }} - fail_ci_if_error: true - verbose: true diff --git a/.github/workflows/generate-pot-file.yml b/.github/workflows/generate-pot-file.yml deleted file mode 100644 index 6647cf1909..0000000000 --- a/.github/workflows/generate-pot-file.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: Regenerate POT file (translatable strings) -on: - schedule: - - cron: "00 16 * * 5" - workflow_dispatch: - -jobs: - regenerate-pot-file: - name: Release - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - branch: ["develop"] - permissions: - contents: write - pull-requests: write - - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - ref: ${{ matrix.branch }} - - - name: Setup Python - uses: actions/setup-python@v6 - with: - python-version: "3.14" - - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 24 - - - name: Run script to update POT file - run: | - bash ${GITHUB_WORKSPACE}/.github/helper/update_pot_file.sh - env: - GH_TOKEN: ${{ github.token }} - BASE_BRANCH: ${{ matrix.branch }} diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index a801daf51d..daedfc23d7 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -2,18 +2,14 @@ name: Linters on: pull_request: - workflow_dispatch: - push: - branches: [ main ] permissions: contents: read jobs: linters: - name: Semgrep Rules + name: Pre-commit and Semgrep runs-on: ubuntu-latest - if: github.event_name == 'pull_request' steps: - uses: actions/checkout@v5 diff --git a/.github/workflows/make_release_pr.yml b/.github/workflows/make_release_pr.yml deleted file mode 100644 index 47f065d8b8..0000000000 --- a/.github/workflows/make_release_pr.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Create weekly release -on: - schedule: - - cron: '30 3 * * 3' - workflow_dispatch: - -permissions: - contents: read - pull-requests: write - -jobs: - release: - name: Release - runs-on: ubuntu-latest - strategy: - fail-fast: false - - steps: - - uses: octokit/request-action@v2.x - with: - route: POST /repos/{owner}/{repo}/pulls - owner: frappe - repo: lms - title: |- - "chore: merge 'main-hotfix' into 'main'" - body: "Automated weekly release" - base: main - head: main-hotfix - env: - GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/on_release.yml b/.github/workflows/on_release.yml deleted file mode 100644 index f6f09319d4..0000000000 --- a/.github/workflows/on_release.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Generate Semantic Release -on: - workflow_dispatch: - push: - branches: - - main - -permissions: - contents: write - issues: write - pull-requests: write - -jobs: - release: - name: Release - runs-on: ubuntu-latest - steps: - - name: Checkout Entire Repository - uses: actions/checkout@v5 - with: - fetch-depth: 0 - persist-credentials: false - - name: Setup Node.js - uses: actions/setup-node@v6 - with: - node-version: 24 - - name: Setup dependencies - run: | - npm install @semantic-release/git @semantic-release/exec --no-save - - name: Create Release - env: - GH_TOKEN: ${{ github.token }} - GITHUB_TOKEN: ${{ github.token }} - GIT_AUTHOR_NAME: "Frappe PR Bot" - GIT_AUTHOR_EMAIL: "developers@frappe.io" - GIT_COMMITTER_NAME: "Frappe PR Bot" - GIT_COMMITTER_EMAIL: "developers@frappe.io" - run: npx semantic-release diff --git a/.github/workflows/semantic.yml b/.github/workflows/semantic.yml deleted file mode 100644 index 5a04c847f7..0000000000 --- a/.github/workflows/semantic.yml +++ /dev/null @@ -1,22 +0,0 @@ -name: Semantic Pull Request - -on: - push: - branches: [ main ] - pull_request: {} - -permissions: - contents: read - -jobs: - # This workflow contains a single job called "build" - semantic: - name: Validate PR title - runs-on: ubuntu-latest - - # Steps represent a sequence of tasks that will be executed as part of the job - steps: - # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it - - uses: actions/checkout@v5 - - - uses: zeke/semantic-pull-requests@main diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml index be09f122aa..fc0bc45c48 100644 --- a/.github/workflows/ui-tests.yml +++ b/.github/workflows/ui-tests.yml @@ -1,13 +1,7 @@ name: UI Tests on: - pull_request: workflow_dispatch: - push: - branches: - - main - - develop - - main-hotfix permissions: contents: read diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 727e4ac56d..deb9785d71 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -41,6 +41,7 @@ declare module 'vue' { Coupons: typeof import('./src/components/Settings/Coupons/Coupons.vue')['default'] CourseCard: typeof import('./src/components/CourseCard.vue')['default'] CourseCardOverlay: typeof import('./src/components/CourseCardOverlay.vue')['default'] + CourseFinalExam: typeof import('./src/components/CourseFinalExam.vue')['default'] CourseInstructors: typeof import('./src/components/CourseInstructors.vue')['default'] CourseOutline: typeof import('./src/components/CourseOutline.vue')['default'] CourseReviews: typeof import('./src/components/CourseReviews.vue')['default'] @@ -60,6 +61,7 @@ declare module 'vue' { EvaluationModal: typeof import('./src/components/Modals/EvaluationModal.vue')['default'] Evaluators: typeof import('./src/components/Settings/Evaluators.vue')['default'] Event: typeof import('./src/components/Modals/Event.vue')['default'] + Exam: typeof import('./src/components/Exam.vue')['default'] ExplanationVideos: typeof import('./src/components/Modals/ExplanationVideos.vue')['default'] FeedbackModal: typeof import('./src/components/Modals/FeedbackModal.vue')['default'] FrappeCloudIcon: typeof import('./src/components/Icons/FrappeCloudIcon.vue')['default'] diff --git a/frontend/src/components/AssessmentPlugin.vue b/frontend/src/components/AssessmentPlugin.vue index a4256a1ca9..43e76eeff1 100644 --- a/frontend/src/components/AssessmentPlugin.vue +++ b/frontend/src/components/AssessmentPlugin.vue @@ -123,10 +123,10 @@ const addAssessment = () => { props.type == 'quiz' ? quiz.value : props.type == 'dragDrop' - ? dragDrop.value - : props.type == 'wordHunt' - ? wordHunt.value - : assignment.value + ? dragDrop.value + : props.type == 'wordHunt' + ? wordHunt.value + : assignment.value ) show.value = false } diff --git a/frontend/src/components/CertificationLinks.vue b/frontend/src/components/CertificationLinks.vue index 1f371def6b..4571c9ed4f 100644 --- a/frontend/src/components/CertificationLinks.vue +++ b/frontend/src/components/CertificationLinks.vue @@ -14,7 +14,8 @@ certification.data && certification.data.membership && certification.data.paid_certificate && - user.data?.is_student + user.data?.is_student && + (!certification.data.final_exam || certification.data.final_exam.passed) " > +
+ {{ __('Pass the final exam to unlock certification.') }} +
diff --git a/frontend/src/components/CourseOutline.vue b/frontend/src/components/CourseOutline.vue index 9ca22b66c1..33bc1dafcc 100644 --- a/frontend/src/components/CourseOutline.vue +++ b/frontend/src/components/CourseOutline.vue @@ -97,19 +97,10 @@
- +
{{ lesson.title }} + + {{ __('Exam') }} + + + {{ __('Locked') }} + (props.accentColor || 'blue').toLowerCase()) +const accentColorName = computed(() => + (props.accentColor || 'blue').toLowerCase() +) const hexToRgb = (hex) => { if (!hex) return '37, 99, 235' @@ -266,17 +271,18 @@ const outlineContainerStyle = computed(() => ? { backgroundColor: alphaColor(0.08), borderColor: alphaColor(0.22), - } + } : {} ) const outline = createResource({ url: 'lms.lms.utils.get_course_outline', - cache: ['course_outline', props.courseName], + cache: ['course_outline', props.courseName, !props.allowEdit], makeParams() { return { course: props.courseName, progress: props.getProgress, + include_exams: !props.allowEdit, } }, auto: true, @@ -447,10 +453,32 @@ const isScormChapterComplete = (chapter) => { return chapter.lessons?.length && chapter.lessons.every((l) => l.is_complete) } -const isActiveLesson = (lessonNumber) => { +const getLessonRoute = (lesson) => { + if (lesson.is_exam) { + return { + name: props.allowEdit ? 'ExamForm' : 'ExamPage', + params: { + examID: lesson.name, + }, + } + } + return { + name: props.allowEdit ? 'LessonForm' : 'Lesson', + params: { + courseName: props.courseName, + chapterNumber: lesson.number.split('-')[0], + lessonNumber: lesson.number.split('-')[1], + }, + } +} + +const isActiveLesson = (lesson) => { + if (lesson.is_exam) { + return route.name === 'ExamPage' && route.params.examID === lesson.name + } return ( - route.params.chapterNumber == lessonNumber.split('-')[0] && - route.params.lessonNumber == lessonNumber.split('-')[1] + route.params.chapterNumber == lesson.number.split('-')[0] && + route.params.lessonNumber == lesson.number.split('-')[1] ) } diff --git a/frontend/src/components/DragDrop.vue b/frontend/src/components/DragDrop.vue index a508ee1c0c..6a7957a7e5 100644 --- a/frontend/src/components/DragDrop.vue +++ b/frontend/src/components/DragDrop.vue @@ -1,6 +1,8 @@