diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index 7c40a9c0c..f63cd5edb 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -64,6 +64,7 @@ jobs: "@addon_block_timeline" "@addon_calendar" "@addon_competency" + "@addon_coursecompletion" "@addon_messages" "@addon_mod_assign" "@addon_mod_bigbluebuttonbn" diff --git a/src/addons/block/completionstatus/services/block-handler.ts b/src/addons/block/completionstatus/services/block-handler.ts index 602be93b2..13cf265c8 100644 --- a/src/addons/block/completionstatus/services/block-handler.ts +++ b/src/addons/block/completionstatus/services/block-handler.ts @@ -18,6 +18,7 @@ import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-tit import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; import { CoreCourseBlock } from '@features/course/services/course'; import { makeSingleton } from '@singletons'; +import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion'; /** * Block handler. @@ -31,7 +32,27 @@ export class AddonBlockCompletionStatusHandlerService extends CoreBlockBaseHandl /** * @inheritdoc */ - getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData { + async isEnabled(): Promise { + return AddonCourseCompletion.isCompletionEnabledInSite(); + } + + /** + * @inheritdoc + */ + async getDisplayData( + block: CoreCourseBlock, + contextLevel: string, + instanceId: number, + ): Promise { + if (contextLevel !== 'course') { + return; + } + + const courseEnabled = await AddonCourseCompletion.isPluginViewEnabledForCourse(instanceId); + if (!courseEnabled) { + return; + } + return { title: 'addon.block_completionstatus.pluginname', class: 'addon-block-completion-status', diff --git a/src/addons/block/selfcompletion/services/block-handler.ts b/src/addons/block/selfcompletion/services/block-handler.ts index 01bed3b35..2d82fe4c0 100644 --- a/src/addons/block/selfcompletion/services/block-handler.ts +++ b/src/addons/block/selfcompletion/services/block-handler.ts @@ -18,6 +18,7 @@ import { CoreBlockOnlyTitleComponent } from '@features/block/components/only-tit import { CoreBlockBaseHandler } from '@features/block/classes/base-block-handler'; import { CoreCourseBlock } from '@features/course/services/course'; import { makeSingleton } from '@singletons'; +import { AddonCourseCompletion } from '@addons/coursecompletion/services/coursecompletion'; /** * Block handler. @@ -31,7 +32,27 @@ export class AddonBlockSelfCompletionHandlerService extends CoreBlockBaseHandler /** * @inheritdoc */ - getDisplayData(block: CoreCourseBlock, contextLevel: string, instanceId: number): CoreBlockHandlerData { + async isEnabled(): Promise { + return AddonCourseCompletion.isCompletionEnabledInSite(); + } + + /** + * @inheritdoc + */ + async getDisplayData( + block: CoreCourseBlock, + contextLevel: string, + instanceId: number, + ): Promise { + if (contextLevel !== 'course') { + return; + } + + const courseEnabled = await AddonCourseCompletion.isPluginViewEnabledForCourse(instanceId); + if (!courseEnabled) { + return; + } + return { title: 'addon.block_selfcompletion.pluginname', class: 'addon-block-self-completion', diff --git a/src/addons/coursecompletion/services/coursecompletion.ts b/src/addons/coursecompletion/services/coursecompletion.ts index 44c1e1e5f..02de42577 100644 --- a/src/addons/coursecompletion/services/coursecompletion.ts +++ b/src/addons/coursecompletion/services/coursecompletion.ts @@ -16,7 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreLogger } from '@singletons/logger'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; -import { CoreCourses } from '@features/courses/services/courses'; +import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses'; import { CoreSite } from '@classes/sites/site'; import { CoreStatusWithWarningsWSResponse, CoreWSExternalWarning } from '@services/ws'; import { makeSingleton } from '@singletons'; @@ -40,6 +40,44 @@ export class AddonCourseCompletionProvider { this.logger = CoreLogger.getInstance('AddonCourseCompletion'); } + /** + * Check whether completion is available in a certain site. + * + * @param site Site. If not defined, use current site. + * @returns True if available. + */ + isCompletionEnabledInSite(site?: CoreSite): boolean { + site = site || CoreSites.getCurrentSite(); + + return !!site && site.canUseAdvancedFeature('enablecompletion'); + } + + /** + * Check whether completion is available in a certain course. + * + * @param course Course. + * @param site Site. If not defined, use current site. + * @returns True if available. + */ + isCompletionEnabledInCourse(course: CoreCourseAnyCourseData, site?: CoreSite): boolean { + if (!this.isCompletionEnabledInSite(site)) { + return false; + } + + return this.isCompletionEnabledInCourseObject(course); + } + + /** + * Check whether completion is enabled in a certain course object. + * + * @param course Course object. + * @returns True if completion is enabled, false otherwise. + */ + protected isCompletionEnabledInCourseObject(course: CoreCourseAnyCourseData): boolean { + // Undefined means it's not supported, so it's enabled by default. + return course.enablecompletion !== false; + } + /** * Returns whether or not the user can mark a course as self completed. * It can if it's configured in the course and it hasn't been completed yet. @@ -180,7 +218,7 @@ export class AddonCourseCompletionProvider { * @returns True if plugin enabled, false otherwise. */ isPluginViewEnabled(): boolean { - return CoreSites.isLoggedIn(); + return CoreSites.isLoggedIn() && this.isCompletionEnabledInSite(); } /** @@ -190,26 +228,19 @@ export class AddonCourseCompletionProvider { * @param preferCache True if shouldn't call WS if data is cached, false otherwise. * @returns Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise. */ - async isPluginViewEnabledForCourse(courseId?: number, preferCache: boolean = true): Promise { - if (!courseId) { + async isPluginViewEnabledForCourse(courseId?: number, preferCache = true): Promise { + if (!courseId || !this.isCompletionEnabledInSite()) { return false; } const course = await CoreCourses.getUserCourse(courseId, preferCache); - if (course) { - if (course.enablecompletion !== undefined && !course.enablecompletion) { - // Completion not enabled for the course. - return false; - } - - if (course.completionhascriteria !== undefined && !course.completionhascriteria) { - // No criteria, cannot view completion. - return false; - } + if (!course) { + return true; } - return true; + // Check completion is enabled in the course and it has criteria, to view completion. + return this.isCompletionEnabledInCourseObject(course) && course.completionhascriteria !== false; } /** diff --git a/src/addons/coursecompletion/tests/behat/navigation.feature b/src/addons/coursecompletion/tests/behat/navigation.feature new file mode 100644 index 000000000..7d8969f16 --- /dev/null +++ b/src/addons/coursecompletion/tests/behat/navigation.feature @@ -0,0 +1,78 @@ +@addon_coursecompletion @app @javascript +Feature: Course completion navigation + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | idnumber | + | teacher1 | Teacher | 1 | teacher1@example.com | T1 | + | student1 | Student | 1 | student1@example.com | S1 | + And the following "courses" exist: + | fullname | shortname | category | enablecompletion | showcompletionconditions | + | Course 1 | C1 | 0 | 1 | 1 | + | Course 2 | C2 | 0 | | | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | teacher1 | C2 | editingteacher | + | student1 | C1 | student | + | student1 | C2 | student | + And the following "activity" exists: + | activity | assign | + | course | C1 | + | name | Test assignment name | + | assignsubmission_onlinetext_enabled | 1 | + | grade[modgrade_type] | Point | + | grade[modgrade_point] | 100 | + | gradepass | 70 | + | completion | 2 | + | completionusegrade | 1 | + | completionpassgrade | 1 | + And the following "activity" exists: + | activity | page | + | course | C2 | + | name | P1 | + And I enable "selfcompletion" "block" plugin + And the following "blocks" exist: + | blockname | contextlevel | reference | + | completionstatus | Course | C1 | + | selfcompletion | Course | C1 | + | activity_modules | Course | C1 | + | completionstatus | Course | C2 | + | selfcompletion | Course | C2 | + | activity_modules | Course | C2 | + And the following config values are set as admin: + | enablecompletion | 1 | + And I am on the "Course 1" course page logged in as teacher1 + And I navigate to "Course completion" in current page administration + And I click on "Condition: Activity completion" "link" + And I set the field "Assignment - Test assignment name" to "1" + And I expand all fieldsets + And I set the following fields to these values: + | id_criteria_self | 1 | + And I press "Save changes" + + Scenario: Completion is available only when enabled for the course + Given I entered the course "Course 1" as "student1" in the app + When I press "Open block drawer" in the app + Then I should find "Course completion status" in the app + And I should find "Self completion" in the app + When I press "Close" in the app + And I press "Completion" in the app + Then I should find "Status" in the app + + Given I entered the course "Course 2" as "student1" in the app + When I press "Open block drawer" in the app + Then I should not find "Course completion status" in the app + And I should not find "Self completion" in the app + When I press "Close" in the app + Then I should not find "Completion" in the app + + Given the following config values are set as admin: + | enablecompletion | 0 | + Then I entered the course "Course 1" as "student1" in the app + And I pull to refresh in the app + When I press "Open block drawer" in the app + Then I should not find "Course completion status" in the app + And I should not find "Self completion" in the app + When I press "Close" in the app + Then I should not find "Completion" in the app diff --git a/src/core/features/course/components/course-index/course-index.ts b/src/core/features/course/components/course-index/course-index.ts index cafce1827..a67118fde 100644 --- a/src/core/features/course/components/course-index/course-index.ts +++ b/src/core/features/course/components/course-index/course-index.ts @@ -22,6 +22,7 @@ import { import { CoreCourseHelper, CoreCourseModuleData, CoreCourseSection } from '@features/course/services/course-helper'; import { CoreCourseFormatDelegate } from '@features/course/services/format-delegate'; import { CoreCourseAnyCourseData } from '@features/courses/services/courses'; +import { CoreCoursesHelper } from '@features/courses/services/courses-helper'; import { CoreSites } from '@services/sites'; import { CoreUtils } from '@services/utils/utils'; import { ModalController } from '@singletons'; @@ -61,7 +62,7 @@ export class CoreCourseCourseIndexComponent implements OnInit { return; } - let completionEnabled = !!this.course.enablecompletion; + let completionEnabled = CoreCoursesHelper.isCompletionEnabledInCourse(this.course); if (completionEnabled && 'completionusertracked' in this.course && this.course.completionusertracked !== undefined) { completionEnabled = this.course.completionusertracked; } diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index ba640a94f..950a509c3 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -37,6 +37,7 @@ import { } from '@singletons/events'; import { CoreNavigator } from '@services/navigator'; import { CoreRefreshContext, CORE_REFRESH_CONTEXT } from '@/core/utils/refresh-context'; +import { CoreCoursesHelper } from '@features/courses/services/courses-helper'; /** * Page that displays the contents of a course. @@ -221,7 +222,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon let completionStatus: Record = {}; // Get the completion status. - if (this.course.enablecompletion !== false) { + if (CoreCoursesHelper.isCompletionEnabledInCourse(this.course)) { const sectionWithModules = sections.find((section) => section.modules.length > 0); if (sectionWithModules && sectionWithModules.modules[0].completion !== undefined) { @@ -265,7 +266,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon protected async loadCourseFormatOptions(): Promise { // Load the course format options when course completion is enabled to show completion progress on sections. - if (!this.course.enablecompletion) { + if (!CoreCoursesHelper.isCompletionEnabledInCourse(this.course)) { return; } diff --git a/src/core/features/courses/services/courses-helper.ts b/src/core/features/courses/services/courses-helper.ts index 0c19e83ea..b066d129a 100644 --- a/src/core/features/courses/services/courses-helper.ts +++ b/src/core/features/courses/services/courses-helper.ts @@ -16,6 +16,7 @@ import { Injectable } from '@angular/core'; import { CoreUtils } from '@services/utils/utils'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; import { + CoreCourseAnyCourseData, CoreCourseAnyCourseDataWithOptions, CoreCourses, CoreCourseSearchedData, @@ -31,6 +32,7 @@ import { zipIncludingComplete } from '@/core/utils/rxjs'; import { catchError, map } from 'rxjs/operators'; import { chainRequests, WSObservable } from '@classes/sites/authenticated-site'; import { LazyRoutesModule } from '@/app/app-routing.module'; +import { CoreSite } from '@classes/sites/site'; // Id for a course item representing all courses (for example, for course filters). export const ALL_COURSES_ID = -1; @@ -363,7 +365,7 @@ export class CoreCoursesHelperProvider { return of(course); } - if (course.enablecompletion !== undefined && !course.enablecompletion) { + if (!this.isCompletionEnabledInCourse(course)) { // Completion is disabled for this course, there is no need to fetch the completion status. return of(course); } @@ -437,6 +439,18 @@ export class CoreCoursesHelperProvider { return import('../courses-my-lazy.module').then(m => m.CoreCoursesMyLazyModule); } + /** + * Check whether completion is available in a certain course. + * This is a temporary function to be used until we move AddonCourseCompletion to core folder (MOBILE-4537). + * + * @param course Course. + * @param site Site. If not defined, use current site. + * @returns True if available. + */ + isCompletionEnabledInCourse(course: CoreCourseAnyCourseData, site?: CoreSite): boolean { + return AddonCourseCompletion.isCompletionEnabledInCourse(course, site); + } + } export const CoreCoursesHelper = makeSingleton(CoreCoursesHelperProvider);