forked from EVOgeek/Vmeda.Online
		
	
						commit
						3036d096aa
					
				| @ -64,7 +64,7 @@ export class AddonBadgesUserHandlerService implements CoreUserProfileHandler { | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         if (navOptions && navOptions.badges !== undefined) { | ||||
|         if (navOptions?.badges !== undefined) { | ||||
|             return navOptions.badges; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -61,7 +61,7 @@ export class AddonBlogCourseOptionHandlerService implements CoreCourseOptionsHan | ||||
|     ): Promise<boolean> { | ||||
|         const enabled = await CoreCourseHelper.hasABlockNamed(courseId, 'blog_menu'); | ||||
| 
 | ||||
|         if (enabled && navOptions && navOptions.blogs !== undefined) { | ||||
|         if (enabled && navOptions?.blogs !== undefined) { | ||||
|             return navOptions.blogs; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreCourseProvider } from '@features/course/services/course'; | ||||
| import { CoreCourseAccessDataType } from '@features/course/services/course'; | ||||
| import { | ||||
|     CoreCourseAccess, | ||||
|     CoreCourseOptionsHandler, | ||||
| @ -50,11 +50,11 @@ export class AddonCompetencyCourseOptionHandlerService implements CoreCourseOpti | ||||
|         accessData: CoreCourseAccess, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<boolean> { | ||||
|         if (accessData && accessData.type === CoreCourseProvider.ACCESS_GUEST) { | ||||
|         if (accessData && accessData.type === CoreCourseAccessDataType.ACCESS_GUEST) { | ||||
|             return false; // Not enabled for guest access.
 | ||||
|         } | ||||
| 
 | ||||
|         if (navOptions && navOptions.competencies !== undefined) { | ||||
|         if (navOptions?.competencies !== undefined) { | ||||
|             return navOptions.competencies; | ||||
|         } | ||||
| 
 | ||||
| @ -82,7 +82,7 @@ export class AddonCompetencyCourseOptionHandlerService implements CoreCourseOpti | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async invalidateEnabledForCourse(courseId: number, navOptions?: CoreCourseUserAdminOrNavOptionIndexed): Promise<void> { | ||||
|         if (navOptions && navOptions.competencies !== undefined) { | ||||
|         if (navOptions?.competencies !== undefined) { | ||||
|             // No need to invalidate anything.
 | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreCourseProvider } from '@features/course/services/course'; | ||||
| import { CoreCourseAccessDataType } from '@features/course/services/course'; | ||||
| import { | ||||
|     CoreCourseAccess, | ||||
|     CoreCourseOptionsHandler, | ||||
| @ -43,7 +43,7 @@ export class AddonCourseCompletionCourseOptionHandlerService implements CoreCour | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     async isEnabledForCourse(courseId: number, accessData: CoreCourseAccess): Promise<boolean> { | ||||
|         if (accessData && accessData.type === CoreCourseProvider.ACCESS_GUEST) { | ||||
|         if (accessData && accessData.type === CoreCourseAccessDataType.ACCESS_GUEST) { | ||||
|             return false; // Not enabled for guest access.
 | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -79,14 +79,11 @@ Feature: Test basic usage of BBB activity in app | ||||
|     And I should not be able to press "Join session" in the app | ||||
| 
 | ||||
|     # Join the session as moderator in a browser. | ||||
|     When I press "Information" in the app | ||||
|     And I press "Open in browser" in the app | ||||
|     And I switch to the browser tab opened by the app | ||||
|     And I log in as "teacher1" | ||||
|     When I open a browser tab with url "$WWWROOT" | ||||
|     And I am on the "bbb1" Activity page logged in as teacher1 | ||||
|     And I click on "Join session" "link" | ||||
|     And I wait for the BigBlueButton room to start | ||||
|     And I switch back to the app | ||||
|     And I press "Close" in the app | ||||
|     And I pull to refresh until I find "The session is in progress" in the app | ||||
|     Then I should find "1" near "Moderator" in the app | ||||
|     And I should find "0" near "Viewer" in the app | ||||
|  | ||||
| @ -31,10 +31,8 @@ Feature: Test basic usage of choice activity in app | ||||
|     Given I entered the choice activity "Choice name" on course "Course 1" as "teacher1" in the app | ||||
|     Then I should find "Test choice description" in the app | ||||
| 
 | ||||
|     When I press "Information" in the app | ||||
|     And I press "Open in browser" in the app | ||||
|     And I switch to the browser tab opened by the app | ||||
|     And I log in as "teacher1" | ||||
|     When I open a browser tab with url "$WWWROOT" | ||||
|     And I am on the "choice1" Activity page logged in as teacher1 | ||||
|     And I press "Actions menu" | ||||
|     And I follow "View 1 responses" | ||||
|     And I press "Download in text format" | ||||
|  | ||||
| @ -176,10 +176,8 @@ Feature: Test basic usage of choice activity in app | ||||
|     Given I entered the choice activity "Choice name" on course "Course 1" as "teacher1" in the app | ||||
|     Then I should find "Test choice description" in the app | ||||
| 
 | ||||
|     When I press "Information" in the app | ||||
|     And I press "Open in browser" in the app | ||||
|     And I switch to the browser tab opened by the app | ||||
|     And I log in as "teacher1" | ||||
|     When I open a browser tab with url "$WWWROOT" | ||||
|     And I am on the "choice1" Activity page logged in as teacher1 | ||||
|     And I follow "Responses" | ||||
|     And I press "Download in text format" | ||||
|     # TODO Then I should find "..." in the downloads folder | ||||
|  | ||||
| @ -138,7 +138,7 @@ export class AddonModFolderIndexComponent extends CoreCourseModuleMainResourceCo | ||||
|             subfolder: folder, | ||||
|         }; | ||||
| 
 | ||||
|         const hash = <string> Md5.hashAsciiStr(folder.filepath); | ||||
|         const hash = Md5.hashAsciiStr(folder.filepath); | ||||
| 
 | ||||
|         CoreNavigator.navigateToSitePath( | ||||
|             `${AddonModFolderModuleHandlerService.PAGE_NAME}/${this.courseId}/${this.module.id}/${hash}`, | ||||
|  | ||||
| @ -207,12 +207,9 @@ Feature: Attempt a quiz in app | ||||
|     And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]" | ||||
|     Then the UI should match the snapshot | ||||
| 
 | ||||
|     Given I entered the quiz activity "Quiz 1" on course "Course 1" as "teacher1" in the app | ||||
|     When I press "Information" in the app | ||||
|     And I press "Open in browser" in the app | ||||
|     And I switch to the browser tab opened by the app | ||||
|     And I log in as "teacher1" | ||||
|     And I follow "Attempts: 1" | ||||
|     Given I open a browser tab with url "$WWWROOT" | ||||
|     And I am on the "quiz1" Activity page logged in as teacher1 | ||||
|     When I follow "Attempts: 1" | ||||
|     And I follow "Review attempt" | ||||
|     Then I should see "Finished" | ||||
|     And I should see "1.00/2.00" | ||||
|  | ||||
| @ -206,11 +206,8 @@ Feature: Attempt a quiz in app | ||||
|     When I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(1) p:nth-child(2)" with "[Started on date]" | ||||
|     And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]" | ||||
| 
 | ||||
|     Given I entered the quiz activity "Quiz 1" on course "Course 1" as "teacher1" in the app | ||||
|     When I press "Information" in the app | ||||
|     And I press "Open in browser" in the app | ||||
|     And I switch to the browser tab opened by the app | ||||
|     And I log in as "teacher1" | ||||
|     Given I open a browser tab with url "$WWWROOT" | ||||
|     When I am on the "quiz1" Activity page logged in as teacher1 | ||||
|     And I follow "Attempts: 1" | ||||
|     And I follow "Review attempt" | ||||
|     Then I should see "Finished" | ||||
|  | ||||
| @ -211,12 +211,9 @@ Feature: Attempt a quiz in app | ||||
|     And I replace "/.*/" within "page-addon-mod-quiz-review core-loading > ion-card ion-item:nth-child(3) p:nth-child(2)" with "[Completed on date]" | ||||
|     Then the UI should match the snapshot | ||||
| 
 | ||||
|     Given I entered the quiz activity "Quiz 1" on course "Course 1" as "teacher1" in the app | ||||
|     When I press "Information" in the app | ||||
|     And I press "Open in browser" in the app | ||||
|     And I switch to the browser tab opened by the app | ||||
|     And I log in as "teacher1" | ||||
|     And I follow "Attempts: 1" | ||||
|     Given I open a browser tab with url "$WWWROOT" | ||||
|     And I am on the "quiz1" Activity page logged in as teacher1 | ||||
|     When I follow "Attempts: 1" | ||||
|     And I follow "Review attempt" | ||||
|     Then I should see "Finished" | ||||
|     And I should see "1.00/2.00" | ||||
|  | ||||
| @ -233,8 +233,7 @@ Feature: Test basic usage of SCORM activity in app | ||||
|     Then I should find "2 / 11" in the app | ||||
| 
 | ||||
|     When I open a browser tab with url "$WWWROOT" | ||||
|     And I log in as "admin" | ||||
|     And I am on the "System logs report" page | ||||
|     And I am on the "System logs report" page logged in as "admin" | ||||
|     And I set the field "id" to "Course 1" | ||||
|     And I set the field "user" to "Student student" | ||||
|     And I press "Get these logs" | ||||
|  | ||||
| @ -638,7 +638,7 @@ export class AddonModWikiIndexComponent extends CoreCourseModuleMainActivityComp | ||||
|      * @returns Promise. | ||||
|      */ | ||||
|     protected async openPageOrSubwiki(options: AddonModWikiOpenPageOptions): Promise<void> { | ||||
|         const hash = <string> Md5.hashAsciiStr(JSON.stringify({ | ||||
|         const hash = Md5.hashAsciiStr(JSON.stringify({ | ||||
|             ...options, | ||||
|             timestamp: Date.now(), | ||||
|         })); | ||||
|  | ||||
| @ -59,7 +59,7 @@ export class AddonModWikiPageOrMapLinkHandlerService extends CoreContentLinksHan | ||||
|                         { siteId, readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE }, | ||||
|                     ); | ||||
| 
 | ||||
|                     const hash = <string> Md5.hashAsciiStr(JSON.stringify({ | ||||
|                     const hash = Md5.hashAsciiStr(JSON.stringify({ | ||||
|                         pageId: page.id, | ||||
|                         pageTitle: page.title, | ||||
|                         subwikiId: page.subwikiid, | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreCourseProvider } from '@features/course/services/course'; | ||||
| import { CoreCourseAccessDataType } from '@features/course/services/course'; | ||||
| import { | ||||
|     CoreCourseAccess, | ||||
|     CoreCourseOptionsHandler, | ||||
| @ -47,11 +47,11 @@ export class AddonNotesCourseOptionHandlerService implements CoreCourseOptionsHa | ||||
|         accessData: CoreCourseAccess, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<boolean> { | ||||
|         if (accessData && accessData.type === CoreCourseProvider.ACCESS_GUEST) { | ||||
|         if (accessData && accessData.type === CoreCourseAccessDataType.ACCESS_GUEST) { | ||||
|             return false; // Not enabled for guest access.
 | ||||
|         } | ||||
| 
 | ||||
|         if (navOptions && navOptions.notes !== undefined) { | ||||
|         if (navOptions?.notes !== undefined) { | ||||
|             return navOptions.notes; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -267,7 +267,7 @@ export class AddonPrivateFilesIndexPage implements OnInit, OnDestroy { | ||||
|             params.filename = ''; | ||||
|         } | ||||
| 
 | ||||
|         const hash = <string> Md5.hashAsciiStr(JSON.stringify(params)); | ||||
|         const hash = Md5.hashAsciiStr(JSON.stringify(params)); | ||||
| 
 | ||||
|         CoreNavigator.navigate(`../${hash}`, { params }); | ||||
|     } | ||||
|  | ||||
| @ -12,32 +12,40 @@ | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Type } from '@angular/core'; | ||||
| /** | ||||
|  * Get core errors exported objects. | ||||
|  * | ||||
|  * @returns Core errors exported objects. | ||||
|  */ | ||||
| export async function getCoreErrorsExportedObjects(): Promise<Record<string, unknown>> { | ||||
| 
 | ||||
| import { CoreError } from './error'; | ||||
| import { CoreWSError } from './wserror'; | ||||
| import { CoreCanceledError } from './cancelederror'; | ||||
| import { CoreSilentError } from './silenterror'; | ||||
| import { CoreAjaxError } from './ajaxerror'; | ||||
| import { CoreAjaxWSError } from './ajaxwserror'; | ||||
| import { CoreCaptureError } from './captureerror'; | ||||
| import { CoreNetworkError } from './network-error'; | ||||
| import { CoreSiteError } from './siteerror'; | ||||
| import { CoreLoginError } from './loginerror'; | ||||
| import { CoreErrorWithOptions } from './errorwithoptions'; | ||||
| import { CoreHttpError } from './httperror'; | ||||
|     const { CoreError } = await import('./error'); | ||||
|     const { CoreWSError } = await import('./wserror'); | ||||
|     const { CoreCanceledError } = await import('./cancelederror'); | ||||
|     const { CoreSilentError } = await import('./silenterror'); | ||||
|     const { CoreAjaxError } = await import('./ajaxerror'); | ||||
|     const { CoreAjaxWSError } = await import('./ajaxwserror'); | ||||
|     const { CoreCaptureError } = await import('./captureerror'); | ||||
|     const { CoreNetworkError } = await import('./network-error'); | ||||
|     const { CoreSiteError } = await import('./siteerror'); | ||||
|     const { CoreLoginError } = await import('./loginerror'); | ||||
|     const { CoreErrorWithOptions } = await import('./errorwithoptions'); | ||||
|     const { CoreHttpError } = await import('./httperror'); | ||||
| 
 | ||||
| export const CORE_ERRORS_CLASSES: Type<unknown>[] = [ | ||||
|     CoreAjaxError, | ||||
|     CoreAjaxWSError, | ||||
|     CoreCanceledError, | ||||
|     CoreCaptureError, | ||||
|     CoreError, | ||||
|     CoreNetworkError, | ||||
|     CoreSilentError, | ||||
|     CoreSiteError, | ||||
|     CoreLoginError, | ||||
|     CoreWSError, | ||||
|     CoreErrorWithOptions, | ||||
|     CoreHttpError, | ||||
| ]; | ||||
|     /* eslint-disable @typescript-eslint/naming-convention */ | ||||
|     return { | ||||
|         CoreError, | ||||
|         CoreWSError, | ||||
|         CoreCanceledError, | ||||
|         CoreSilentError, | ||||
|         CoreAjaxError, | ||||
|         CoreAjaxWSError, | ||||
|         CoreCaptureError, | ||||
|         CoreNetworkError, | ||||
|         CoreSiteError, | ||||
|         CoreLoginError, | ||||
|         CoreErrorWithOptions, | ||||
|         CoreHttpError, | ||||
|     }; | ||||
|     /* eslint-enable @typescript-eslint/naming-convention */ | ||||
| } | ||||
|  | ||||
| @ -1010,7 +1010,7 @@ export class CoreAuthenticatedSite extends CoreUnauthenticatedSite { | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
|     protected getCacheId(method: string, data: any): string { | ||||
|         return <string> Md5.hashAsciiStr(method + ':' + CoreUtils.sortAndStringify(data)); | ||||
|         return Md5.hashAsciiStr(method + ':' + CoreUtils.sortAndStringify(data)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -171,7 +171,7 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { | ||||
|     protected async getComponentClass(): Promise<Type<unknown>> { | ||||
|         // eslint-disable-next-line @typescript-eslint/no-this-alias
 | ||||
|         const compileInstance = this; | ||||
|         const lazyLibraries = await CoreCompile.getLazyLibraries(); | ||||
|         await CoreCompile.loadLibraries(); | ||||
| 
 | ||||
|         // Create the component, using the text as the template.
 | ||||
|         return class CoreCompileHtmlFakeComponent implements OnInit, AfterContentInit, AfterViewInit, OnDestroy { | ||||
| @ -187,10 +187,10 @@ export class CoreCompileHtmlComponent implements OnChanges, OnDestroy, DoCheck { | ||||
|                 this['dataArray'] = []; | ||||
| 
 | ||||
|                 // Inject the libraries.
 | ||||
|                 CoreCompile.injectLibraries(this, [ | ||||
|                     ...lazyLibraries, | ||||
|                     ...compileInstance.extraProviders, | ||||
|                 ]); | ||||
|                 CoreCompile.injectLibraries( | ||||
|                     this, | ||||
|                     compileInstance.extraProviders, | ||||
|                 ); | ||||
| 
 | ||||
|                 // Always add these elements, they could be needed on component init (componentObservable).
 | ||||
|                 this['ChangeDetectorRef'] = compileInstance.changeDetector; | ||||
|  | ||||
| @ -43,8 +43,8 @@ import { makeSingleton } from '@singletons'; | ||||
| import { getCoreServices } from '@/core/core.module'; | ||||
| import { getBlockServices } from '@features/block/block.module'; | ||||
| import { getCommentsServices } from '@features/comments/comments.module'; | ||||
| import { getContentLinksServices } from '@features/contentlinks/contentlinks.module'; | ||||
| import { getCourseServices } from '@features/course/course.module'; | ||||
| import { getContentLinksExportedObjects, getContentLinksServices } from '@features/contentlinks/contentlinks.module'; | ||||
| import { getCourseExportedObjects, getCourseServices } from '@features/course/course.module'; | ||||
| import { getCoursesServices } from '@features/courses/courses.module'; | ||||
| import { getEditorServices } from '@features/editor/editor.module'; | ||||
| import { getEnrolServices } from '@features/enrol/enrol.module'; | ||||
| @ -88,13 +88,8 @@ import { CoreUrl } from '@singletons/url'; | ||||
| import { CoreWindow } from '@singletons/window'; | ||||
| import { CoreCache } from '@classes/cache'; | ||||
| import { CoreDelegate } from '@classes/delegate'; | ||||
| import { CoreContentLinksHandlerBase } from '@features/contentlinks/classes/base-handler'; | ||||
| import { CoreContentLinksModuleGradeHandler } from '@features/contentlinks/classes/module-grade-handler'; | ||||
| import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler'; | ||||
| import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; | ||||
| import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler'; | ||||
| import { CoreGeolocationError, CoreGeolocationErrorReason } from '@services/geolocation'; | ||||
| import { CORE_ERRORS_CLASSES } from '@classes/errors/errors'; | ||||
| import { getCoreErrorsExportedObjects } from '@classes/errors/errors'; | ||||
| import { CoreNetwork } from '@services/network'; | ||||
| 
 | ||||
| // Import all core modules that define components, directives and pipes.
 | ||||
| @ -109,19 +104,6 @@ import { CoreBlockComponentsModule } from '@features/block/components/components | ||||
| import { CoreEditorComponentsModule } from '@features/editor/components/components.module'; | ||||
| import { CoreSearchComponentsModule } from '@features/search/components/components.module'; | ||||
| 
 | ||||
| // Import some components so they can be injected dynamically.
 | ||||
| import { CoreCourseUnsupportedModuleComponent } from '@features/course/components/unsupported-module/unsupported-module'; | ||||
| import { CoreCourseFormatSingleActivityComponent } from '@features/course/format/singleactivity/components/singleactivity'; | ||||
| import { CoreSitePluginsModuleIndexComponent } from '@features/siteplugins/components/module-index/module-index'; | ||||
| import { CoreSitePluginsBlockComponent } from '@features/siteplugins/components/block/block'; | ||||
| import { CoreSitePluginsCourseFormatComponent } from '@features/siteplugins/components/course-format/course-format'; | ||||
| import { CoreSitePluginsQuestionComponent } from '@features/siteplugins/components/question/question'; | ||||
| import { CoreSitePluginsQuestionBehaviourComponent } from '@features/siteplugins/components/question-behaviour/question-behaviour'; | ||||
| import { CoreSitePluginsUserProfileFieldComponent } from '@features/siteplugins/components/user-profile-field/user-profile-field'; | ||||
| import { CoreSitePluginsQuizAccessRuleComponent } from '@features/siteplugins/components/quiz-access-rule/quiz-access-rule'; | ||||
| import { CoreSitePluginsAssignFeedbackComponent } from '@features/siteplugins/components/assign-feedback/assign-feedback'; | ||||
| import { CoreSitePluginsAssignSubmissionComponent } from '@features/siteplugins/components/assign-submission/assign-submission'; | ||||
| 
 | ||||
| // Import addon providers. Do not import database module because it causes circular dependencies.
 | ||||
| import { getBadgesServices } from '@addons/badges/badges.module'; | ||||
| import { getCalendarServices } from '@addons/calendar/calendar.module'; | ||||
| @ -142,6 +124,8 @@ import { CorePlatform } from '@services/platform'; | ||||
| 
 | ||||
| import { CoreAutoLogoutService } from '@features/autologout/services/autologout'; | ||||
| import { CoreSitePluginsProvider } from '@features/siteplugins/services/siteplugins'; | ||||
| import { getSitePluginsExportedObjects } from '@features/siteplugins/siteplugins.module'; | ||||
| import { CoreError } from '@classes/errors/error'; | ||||
| 
 | ||||
| /** | ||||
|  * Service to provide functionalities regarding compiling dynamic HTML and Javascript. | ||||
| @ -177,6 +161,9 @@ export class CoreCompileProvider { | ||||
|         getModWorkshopComponentModules, | ||||
|     ]; | ||||
| 
 | ||||
|     protected libraries?: unknown[]; | ||||
|     protected exportedObjects?: Record<string, unknown>; | ||||
| 
 | ||||
|     constructor(protected injector: Injector) { | ||||
|         this.logger = CoreLogger.getInstance('CoreCompileProvider'); | ||||
|     } | ||||
| @ -264,26 +251,28 @@ export class CoreCompileProvider { | ||||
|      * Inject all the core libraries in a certain object. | ||||
|      * | ||||
|      * @param instance The instance where to inject the libraries. | ||||
|      * @param extraProviders Extra imported providers if needed and not imported by this class. | ||||
|      * @param extraLibraries Extra imported providers if needed and not imported by this class. | ||||
|      */ | ||||
|     // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||
|     injectLibraries(instance: any, extraProviders: Type<unknown>[] = []): void { | ||||
|         const providers = [ | ||||
|             ...extraProviders, | ||||
|             CoreAutoLogoutService, | ||||
|             CoreSitePluginsProvider, | ||||
|             ...this.OTHER_SERVICES, | ||||
|     injectLibraries(instance: any, extraLibraries: Type<unknown>[] = []): void { | ||||
|         if (!this.libraries || !this.exportedObjects) { | ||||
|             throw new CoreError('Libraries not loaded. You need to call loadLibraries before calling injectLibraries.'); | ||||
|         } | ||||
| 
 | ||||
|         const libraries = [ | ||||
|             ...this.libraries, | ||||
|             ...extraLibraries, | ||||
|         ]; | ||||
| 
 | ||||
|         // We cannot inject anything to this constructor. Use the Injector to inject all the providers into the instance.
 | ||||
|         for (const i in providers) { | ||||
|             const providerDef = providers[i]; | ||||
|             if (typeof providerDef === 'function' && providerDef.name) { | ||||
|         for (const i in libraries) { | ||||
|             const libraryDef = libraries[i]; | ||||
|             if (typeof libraryDef === 'function' && libraryDef.name) { | ||||
|                 try { | ||||
|                     // Inject the provider to the instance. We use the class name as the property name.
 | ||||
|                     instance[providerDef.name.replace(/DelegateService$/, 'Delegate')] = this.injector.get<Provider>(providerDef); | ||||
|                     instance[libraryDef.name.replace(/DelegateService$/, 'Delegate')] = this.injector.get<Provider>(libraryDef); | ||||
|                 } catch (ex) { | ||||
|                     this.logger.error('Error injecting provider', providerDef.name, ex); | ||||
|                     this.logger.error('Error injecting provider', libraryDef.name, ex); | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
| @ -319,27 +308,26 @@ export class CoreCompileProvider { | ||||
|         instance['CoreCache'] = CoreCache; // @deprecated since 4.4, plugins should use plain objects instead.
 | ||||
|         instance['CoreDelegate'] = CoreDelegate; | ||||
|         instance['CorePromisedValue'] = CorePromisedValue; | ||||
|         instance['CoreContentLinksHandlerBase'] = CoreContentLinksHandlerBase; | ||||
|         instance['CoreContentLinksModuleGradeHandler'] = CoreContentLinksModuleGradeHandler; | ||||
|         instance['CoreContentLinksModuleIndexHandler'] = CoreContentLinksModuleIndexHandler; | ||||
|         instance['CoreCourseActivityPrefetchHandlerBase'] = CoreCourseActivityPrefetchHandlerBase; | ||||
|         instance['CoreCourseResourcePrefetchHandlerBase'] = CoreCourseResourcePrefetchHandlerBase; | ||||
|         instance['CoreCourseUnsupportedModuleComponent'] = CoreCourseUnsupportedModuleComponent; | ||||
|         instance['CoreCourseFormatSingleActivityComponent'] = CoreCourseFormatSingleActivityComponent; | ||||
|         instance['CoreSitePluginsModuleIndexComponent'] = CoreSitePluginsModuleIndexComponent; | ||||
|         instance['CoreSitePluginsBlockComponent'] = CoreSitePluginsBlockComponent; | ||||
|         instance['CoreSitePluginsCourseFormatComponent'] = CoreSitePluginsCourseFormatComponent; | ||||
|         instance['CoreSitePluginsQuestionComponent'] = CoreSitePluginsQuestionComponent; | ||||
|         instance['CoreSitePluginsQuestionBehaviourComponent'] = CoreSitePluginsQuestionBehaviourComponent; | ||||
|         instance['CoreSitePluginsUserProfileFieldComponent'] = CoreSitePluginsUserProfileFieldComponent; | ||||
|         instance['CoreSitePluginsQuizAccessRuleComponent'] = CoreSitePluginsQuizAccessRuleComponent; | ||||
|         instance['CoreSitePluginsAssignFeedbackComponent'] = CoreSitePluginsAssignFeedbackComponent; | ||||
|         instance['CoreSitePluginsAssignSubmissionComponent'] = CoreSitePluginsAssignSubmissionComponent; | ||||
|         instance['CoreGeolocationError'] = CoreGeolocationError; | ||||
|         instance['CoreGeolocationErrorReason'] = CoreGeolocationErrorReason; | ||||
|         CORE_ERRORS_CLASSES.forEach((classDef) => { | ||||
|             instance[classDef.name] = classDef; | ||||
|         }); | ||||
| 
 | ||||
|         // Inject exported objects.
 | ||||
|         for (const name in this.exportedObjects) { | ||||
|             instance[name] = this.exportedObjects[name]; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Load all the libraries needed for the compile service. | ||||
|      */ | ||||
|     async loadLibraries(): Promise<void> { | ||||
|         if (!this.libraries) { | ||||
|             this.libraries = await this.getLibraries(); | ||||
|         } | ||||
| 
 | ||||
|         if (!this.exportedObjects) { | ||||
|             this.exportedObjects = await this.getExportedObjects(); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -347,7 +335,7 @@ export class CoreCompileProvider { | ||||
|      * | ||||
|      * @returns Lazy libraries. | ||||
|      */ | ||||
|     async getLazyLibraries(): Promise<Type<unknown>[]> { | ||||
|     protected async getLibraries(): Promise<unknown[]> { | ||||
|         const services = await Promise.all([ | ||||
|             getCoreServices(), | ||||
|             getBlockServices(), | ||||
| @ -389,7 +377,30 @@ export class CoreCompileProvider { | ||||
|             getPrivateFilesServices(), | ||||
|         ]); | ||||
| 
 | ||||
|         return services.flat(); | ||||
|         const lazyLibraries = services.flat(); | ||||
| 
 | ||||
|         return [ | ||||
|             ...lazyLibraries, | ||||
|             CoreAutoLogoutService, | ||||
|             CoreSitePluginsProvider, | ||||
|             ...this.OTHER_SERVICES, | ||||
|         ]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get lazy exported objects to inject. | ||||
|      * | ||||
|      * @returns Lazy exported objects. | ||||
|      */ | ||||
|     protected async getExportedObjects(): Promise<Record<string, unknown>> { | ||||
|         const objects = await Promise.all([ | ||||
|             getCoreErrorsExportedObjects(), | ||||
|             getCourseExportedObjects(), | ||||
|             getContentLinksExportedObjects(), | ||||
|             getSitePluginsExportedObjects(), | ||||
|         ]); | ||||
| 
 | ||||
|         return Object.assign({}, ...objects); | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -30,6 +30,25 @@ export async function getContentLinksServices(): Promise<Type<unknown>[]> { | ||||
|     ]; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Get content links exported objects. | ||||
|  * | ||||
|  * @returns Content links exported objects. | ||||
|  */ | ||||
| export async function getContentLinksExportedObjects(): Promise<Record<string, unknown>> { | ||||
|     const { CoreContentLinksHandlerBase } = await import ('@features/contentlinks/classes/base-handler'); | ||||
|     const { CoreContentLinksModuleGradeHandler } = await import ('@features/contentlinks/classes/module-grade-handler'); | ||||
|     const { CoreContentLinksModuleIndexHandler } = await import ('@features/contentlinks/classes/module-index-handler'); | ||||
| 
 | ||||
|     /* eslint-disable @typescript-eslint/naming-convention */ | ||||
|     return { | ||||
|         CoreContentLinksHandlerBase, | ||||
|         CoreContentLinksModuleGradeHandler, | ||||
|         CoreContentLinksModuleIndexHandler, | ||||
|     }; | ||||
|     /* eslint-enable @typescript-eslint/naming-convention */ | ||||
| } | ||||
| 
 | ||||
| @NgModule({ | ||||
|     imports: [ | ||||
|         CoreContentLinksComponentsModule, | ||||
|  | ||||
| @ -24,7 +24,8 @@ | ||||
|                 </h1> | ||||
|             </ion-label> | ||||
|             <ion-button fill="clear" *ngIf="displayOptions.displayOpenInBrowser && externalUrl" [href]="externalUrl" core-link | ||||
|                 [showBrowserWarning]="false" [attr.aria-label]="'core.openinbrowser' | translate" slot="end"> | ||||
|                 [showBrowserWarning]="false" [attr.aria-label]="'core.openinbrowser' | translate" slot="end" [class.hidden]="!isTeacher" | ||||
|                 class="core-module-oib-button"> | ||||
|                 <ion-icon name="fas-up-right-from-square" slot="icon-only" aria-hidden="true" /> | ||||
|             </ion-button> | ||||
|         </ion-item> | ||||
|  | ||||
| @ -39,3 +39,7 @@ ion-item.card-header { | ||||
|         margin: 0px; | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| .core-module-oib-button.hidden { | ||||
|     display: none; | ||||
| } | ||||
|  | ||||
| @ -70,6 +70,7 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy { | ||||
|     course?: CoreEnrolledCourseData; | ||||
|     modicon = ''; | ||||
|     moduleNameTranslated = ''; | ||||
|     isTeacher = false; | ||||
| 
 | ||||
|     protected onlineSubscription: Subscription; // It will observe the status of the network connection.
 | ||||
|     protected packageStatusObserver?: CoreEventObserver; // Observer of package status.
 | ||||
| @ -269,13 +270,14 @@ export class CoreCourseModuleSummaryComponent implements OnInit, OnDestroy { | ||||
|      * Fetch course. | ||||
|      */ | ||||
|     protected async fetchCourse(): Promise<void> { | ||||
|         // Fix that.
 | ||||
|         try { | ||||
|             this.course = await CoreCourses.getUserCourse(this.courseId, true); | ||||
|         } catch { | ||||
|             // The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course.
 | ||||
|             this.course = await CoreCourses.getCourse(this.courseId); | ||||
|         } | ||||
| 
 | ||||
|         this.isTeacher = await CoreUtils.ignoreErrors(CoreCourseHelper.guessIsTeacher(this.courseId, this.course), false); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -62,6 +62,31 @@ export async function getCourseServices(): Promise<Type<unknown>[]> { | ||||
|     ]; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Get course exported objects. | ||||
|  * | ||||
|  * @returns Course exported objects. | ||||
|  */ | ||||
| export async function getCourseExportedObjects(): Promise<Record<string, unknown>> { | ||||
|     const { CoreCourseActivityPrefetchHandlerBase } = await import('@features/course/classes/activity-prefetch-handler'); | ||||
|     const { CoreCourseResourcePrefetchHandlerBase } = await import('@features/course/classes/resource-prefetch-handler'); | ||||
|     const { CoreCourseAccessDataType } = await import('@features/course/services/course'); | ||||
|     const { CoreCourseUnsupportedModuleComponent } = | ||||
|         await import ('@features/course/components/unsupported-module/unsupported-module'); | ||||
|     const { CoreCourseFormatSingleActivityComponent } = | ||||
|         await import ('@features/course/format/singleactivity/components/singleactivity'); | ||||
| 
 | ||||
|     /* eslint-disable @typescript-eslint/naming-convention */ | ||||
|     return { | ||||
|         CoreCourseActivityPrefetchHandlerBase, | ||||
|         CoreCourseResourcePrefetchHandlerBase, | ||||
|         CoreCourseUnsupportedModuleComponent, | ||||
|         CoreCourseFormatSingleActivityComponent, | ||||
|         CoreCourseAccessDataType, | ||||
|     }; | ||||
|     /* eslint-enable @typescript-eslint/naming-convention */ | ||||
| } | ||||
| 
 | ||||
| export const COURSE_PAGE_NAME = 'course'; | ||||
| export const CONTENTS_PAGE_NAME = 'contents'; | ||||
| export const COURSE_CONTENTS_PATH = `${COURSE_PAGE_NAME}/${COURSE_INDEX_PATH}/${CONTENTS_PAGE_NAME}`; | ||||
|  | ||||
| @ -41,7 +41,8 @@ | ||||
|                         </ion-chip> | ||||
|                     </ion-label> | ||||
|                     <ion-button *ngIf="displayOpenInBrowser" fill="clear" [href]="courseUrl" core-link [showBrowserWarning]="false" | ||||
|                         [attr.aria-label]="'core.openinbrowser' | translate" slot="end"> | ||||
|                         [attr.aria-label]="'core.openinbrowser' | translate" slot="end" [class.hidden]="!isTeacher" | ||||
|                         class="core-course-oib-button"> | ||||
|                         <ion-icon name="fas-up-right-from-square" slot="icon-only" aria-hidden="true" /> | ||||
|                     </ion-button> | ||||
|                 </ion-item> | ||||
|  | ||||
| @ -73,6 +73,7 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { | ||||
|     progress?: number; | ||||
|     courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = []; | ||||
|     displayOpenInBrowser = false; | ||||
|     isTeacher = false; | ||||
| 
 | ||||
|     protected actionSheet?: HTMLIonActionSheetElement; | ||||
|     protected waitStart = 0; | ||||
| @ -172,6 +173,9 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { | ||||
| 
 | ||||
|         await this.loadMenuHandlers(refresh); | ||||
| 
 | ||||
|         // After loading menu handlers, admOptions should be available.
 | ||||
|         this.isTeacher = await CoreUtils.ignoreErrors(CoreCourseHelper.guessIsTeacher(this.courseId, this.course), false); | ||||
| 
 | ||||
|         this.dataLoaded = true; | ||||
|     } | ||||
| 
 | ||||
| @ -254,7 +258,7 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { | ||||
|      * @returns Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadMenuHandlers(refresh?: boolean): Promise<void> { | ||||
|         if (!this.course) { | ||||
|         if (!this.course || !this.canAccessCourse) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -104,4 +104,8 @@ | ||||
|             display: inline; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     .core-course-oib-button.hidden { | ||||
|         display: none; | ||||
|     } | ||||
| } | ||||
|  | ||||
| @ -2058,6 +2058,30 @@ export class CoreCourseHelperProvider { | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Guess if the user is a teacher in a course. | ||||
|      * | ||||
|      * @param courseId Course Id. | ||||
|      * @param course Course object. | ||||
|      * @returns Promise resolved with boolean: whether the user is a teacher. | ||||
|      */ | ||||
|     async guessIsTeacher( | ||||
|         courseId: number, | ||||
|         course?: CoreEnrolledCourseData | CoreCourseSearchedData, | ||||
|     ): Promise<boolean> { | ||||
|         if (course && 'admOptions' in course && course.admOptions) { | ||||
|             return !!course.admOptions['reports']; | ||||
|         } | ||||
| 
 | ||||
|         // Not loaded yet, try to load it.
 | ||||
|         const adminOptions = await CoreCourses.getUserAdministrationOptions( | ||||
|             [courseId], | ||||
|             { readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE }, | ||||
|         ); | ||||
| 
 | ||||
|         return !!adminOptions[courseId]?.['reports']; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export const CoreCourseHelper = makeSingleton(CoreCourseHelperProvider); | ||||
|  | ||||
| @ -24,10 +24,9 @@ import { | ||||
|     CoreCoursesProvider, | ||||
|     CoreCourseUserAdminOrNavOptionIndexed, | ||||
| } from '@features/courses/services/courses'; | ||||
| import { CoreCourseProvider } from './course'; | ||||
| import { CoreCourseAccessDataType } from './course'; | ||||
| import { Params } from '@angular/router'; | ||||
| import { makeSingleton } from '@singletons'; | ||||
| import { CoreEnrolledCourseDataWithExtraInfoAndOptions } from '@features/courses/services/courses-helper'; | ||||
| import { CorePromisedValue } from '@classes/promised-value'; | ||||
| 
 | ||||
| /** | ||||
| @ -313,20 +312,20 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|      * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. | ||||
|      * @returns Promise resolved with array of handlers. | ||||
|      */ | ||||
|     protected async getHandlersForAccess( | ||||
|     protected async updateHandlersForAccess( | ||||
|         courseId: number, | ||||
|         refresh: boolean, | ||||
|         accessData: CoreCourseAccess, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|         admOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<CoreCourseOptionsHandler[]> { | ||||
|     ): Promise<void> { | ||||
| 
 | ||||
|         // If the handlers aren't loaded, do not refresh.
 | ||||
|         if (!this.loaded[courseId]) { | ||||
|             refresh = false; | ||||
|         } | ||||
| 
 | ||||
|         if (refresh || !this.coursesHandlers[courseId] || this.coursesHandlers[courseId].access.type != accessData.type) { | ||||
|         if (refresh || !this.coursesHandlers[courseId] || this.coursesHandlers[courseId].access.type !== accessData.type) { | ||||
|             if (!this.coursesHandlers[courseId]) { | ||||
|                 this.coursesHandlers[courseId] = { | ||||
|                     access: accessData, | ||||
| @ -347,8 +346,6 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|         } | ||||
| 
 | ||||
|         await this.coursesHandlers[courseId].deferred; | ||||
| 
 | ||||
|         return this.coursesHandlers[courseId].enabledHandlers; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -358,18 +355,14 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|      * @param course The course object. | ||||
|      * @param refresh True if it should refresh the list. | ||||
|      * @param isGuest Whether user is using an ACCESS_GUEST enrolment method. | ||||
|      * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. | ||||
|      * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. | ||||
|      * @returns Promise resolved with array of handlers. | ||||
|      */ | ||||
|     getHandlersToDisplay( | ||||
|         course: CoreCourseAnyCourseData, | ||||
|         refresh = false, | ||||
|         isGuest = false, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|         admOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<CoreCourseOptionsHandlerToDisplay[]> { | ||||
|         return this.getHandlersToDisplayInternal(false, course, refresh, isGuest, navOptions, admOptions) as | ||||
|         return this.getHandlersToDisplayInternal(false, course, refresh, isGuest) as | ||||
|             Promise<CoreCourseOptionsHandlerToDisplay[]>; | ||||
|     } | ||||
| 
 | ||||
| @ -380,18 +373,14 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|      * @param course The course object. | ||||
|      * @param refresh True if it should refresh the list. | ||||
|      * @param isGuest Whether user is using an ACCESS_GUEST enrolment method. | ||||
|      * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. | ||||
|      * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. | ||||
|      * @returns Promise resolved with array of handlers. | ||||
|      */ | ||||
|     getMenuHandlersToDisplay( | ||||
|         course: CoreCourseAnyCourseData, | ||||
|         refresh = false, | ||||
|         isGuest = false, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|         admOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<CoreCourseOptionsMenuHandlerToDisplay[]> { | ||||
|         return this.getHandlersToDisplayInternal(true, course, refresh, isGuest, navOptions, admOptions) as | ||||
|         return this.getHandlersToDisplayInternal(true, course, refresh, isGuest) as | ||||
|             Promise<CoreCourseOptionsMenuHandlerToDisplay[]>; | ||||
|     } | ||||
| 
 | ||||
| @ -403,8 +392,6 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|      * @param course The course object. | ||||
|      * @param refresh True if it should refresh the list. | ||||
|      * @param isGuest Whether user is using an ACCESS_GUEST enrolment method. | ||||
|      * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. | ||||
|      * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. | ||||
|      * @returns Promise resolved with array of handlers. | ||||
|      */ | ||||
|     protected async getHandlersToDisplayInternal( | ||||
| @ -412,36 +399,30 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|         course: CoreCourseAnyCourseData, | ||||
|         refresh = false, | ||||
|         isGuest = false, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|         admOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[]> { | ||||
| 
 | ||||
|         const courseWithOptions: CoreCourseAnyCourseDataWithOptions = course; | ||||
|         const accessData = { | ||||
|             type: isGuest ? CoreCourseProvider.ACCESS_GUEST : CoreCourseProvider.ACCESS_DEFAULT, | ||||
|             type: isGuest ? CoreCourseAccessDataType.ACCESS_GUEST : CoreCourseAccessDataType.ACCESS_DEFAULT, | ||||
|         }; | ||||
|         const handlersToDisplay: CoreCourseOptionsHandlerToDisplay[] | CoreCourseOptionsMenuHandlerToDisplay[] = []; | ||||
| 
 | ||||
|         if (navOptions) { | ||||
|             courseWithOptions.navOptions = navOptions; | ||||
|         } | ||||
|         if (admOptions) { | ||||
|             courseWithOptions.admOptions = admOptions; | ||||
|         } | ||||
| 
 | ||||
|         await this.loadCourseOptions(courseWithOptions, refresh); | ||||
| 
 | ||||
|         // Call getHandlersForAccess to make sure the handlers have been loaded.
 | ||||
|         await this.getHandlersForAccess(course.id, refresh, accessData, courseWithOptions.navOptions, courseWithOptions.admOptions); | ||||
|         // Call updateHandlersForAccess to make sure the handlers have been loaded.
 | ||||
|         await this.updateHandlersForAccess( | ||||
|             course.id, | ||||
|             refresh, | ||||
|             accessData, | ||||
|             courseWithOptions.navOptions, | ||||
|             courseWithOptions.admOptions, | ||||
|         ); | ||||
| 
 | ||||
|         const promises: Promise<void>[] = []; | ||||
| 
 | ||||
|         let handlerList: CoreCourseOptionsMenuHandler[] | CoreCourseOptionsHandler[]; | ||||
|         if (menu) { | ||||
|             handlerList = this.coursesHandlers[course.id].enabledMenuHandlers; | ||||
|         } else { | ||||
|             handlerList = this.coursesHandlers[course.id].enabledHandlers; | ||||
|         } | ||||
|         const handlerList = menu | ||||
|             ? this.coursesHandlers[course.id].enabledMenuHandlers | ||||
|             : this.coursesHandlers[course.id].enabledHandlers; | ||||
| 
 | ||||
|         handlerList.forEach((handler: CoreCourseOptionsMenuHandler | CoreCourseOptionsHandler) => { | ||||
|             const getFunction = menu | ||||
| @ -461,8 +442,8 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|                 }); | ||||
| 
 | ||||
|                 return; | ||||
|             }).catch((err) => { | ||||
|                 this.logger.error('Error getting data for handler', handler.name, err); | ||||
|             }).catch((error) => { | ||||
|                 this.logger.error(`Error getting data for handler ${handler.name}`, error); | ||||
|             })); | ||||
|         }); | ||||
| 
 | ||||
| @ -477,17 +458,44 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|         return handlersToDisplay; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Get the handlers for a course using a certain access type. | ||||
|      * | ||||
|      * @param courseId The course ID. | ||||
|      * @param refresh True if it should refresh the list. | ||||
|      * @param accessData Access type and data. Default, guest, ... | ||||
|      * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. | ||||
|      * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. | ||||
|      * @returns Promise resolved with array of handlers. | ||||
|      * @deprecated since 4.4. | ||||
|      */ | ||||
|     protected async hasHandlersForAccess( | ||||
|         courseId: number, | ||||
|         refresh: boolean, | ||||
|         accessData: CoreCourseAccess, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|         admOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<boolean> { | ||||
|         await this.updateHandlersForAccess(courseId, refresh, accessData, navOptions, admOptions); | ||||
| 
 | ||||
|         const handlers = this.coursesHandlers[courseId].enabledHandlers; | ||||
| 
 | ||||
|         return !!(handlers && handlers.length); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Check if a course has any handler enabled for default access, using course object. | ||||
|      * | ||||
|      * @param course The course object. | ||||
|      * @param refresh True if it should refresh the list. | ||||
|      * @returns Promise resolved with boolean: true if it has handlers, false otherwise. | ||||
|      * @deprecated since 4.4. | ||||
|      */ | ||||
|     async hasHandlersForCourse(course: CoreEnrolledCourseDataWithExtraInfoAndOptions, refresh = false): Promise<boolean> { | ||||
|     async hasHandlersForCourse(course: CoreCourseAnyCourseDataWithOptions, refresh = false): Promise<boolean> { | ||||
|         // Load course options if missing.
 | ||||
|         await this.loadCourseOptions(course, refresh); | ||||
| 
 | ||||
|         // eslint-disable-next-line deprecation/deprecation
 | ||||
|         return this.hasHandlersForDefault(course.id, refresh, course.navOptions, course.admOptions); | ||||
|     } | ||||
| 
 | ||||
| @ -499,6 +507,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|      * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. | ||||
|      * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. | ||||
|      * @returns Promise resolved with boolean: true if it has handlers, false otherwise. | ||||
|      * @deprecated since 4.4. | ||||
|      */ | ||||
|     async hasHandlersForDefault( | ||||
|         courseId: number, | ||||
| @ -506,14 +515,14 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|         admOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<boolean> { | ||||
|         // Default access.
 | ||||
|         const accessData = { | ||||
|             type: CoreCourseProvider.ACCESS_DEFAULT, | ||||
|         }; | ||||
| 
 | ||||
|         const handlers = await this.getHandlersForAccess(courseId, refresh, accessData, navOptions, admOptions); | ||||
| 
 | ||||
|         return !!(handlers && handlers.length); | ||||
|         // eslint-disable-next-line deprecation/deprecation
 | ||||
|         return await this.hasHandlersForAccess( | ||||
|             courseId, | ||||
|             refresh, | ||||
|             { type: CoreCourseAccessDataType.ACCESS_DEFAULT }, | ||||
|             navOptions, | ||||
|             admOptions, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -524,6 +533,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|      * @param navOptions Course navigation options for current user. See CoreCoursesProvider.getUserNavigationOptions. | ||||
|      * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. | ||||
|      * @returns Promise resolved with boolean: true if it has handlers, false otherwise. | ||||
|      * @deprecated since 4.4. | ||||
|      */ | ||||
|     async hasHandlersForGuest( | ||||
|         courseId: number, | ||||
| @ -531,14 +541,14 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|         admOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<boolean> { | ||||
|         // Guest access.
 | ||||
|         const accessData = { | ||||
|             type: CoreCourseProvider.ACCESS_GUEST, | ||||
|         }; | ||||
| 
 | ||||
|         const handlers = await this.getHandlersForAccess(courseId, refresh, accessData, navOptions, admOptions); | ||||
| 
 | ||||
|         return !!(handlers && handlers.length); | ||||
|         // eslint-disable-next-line deprecation/deprecation
 | ||||
|         return await this.hasHandlersForAccess( | ||||
|             courseId, | ||||
|             refresh, | ||||
|             { type: CoreCourseAccessDataType.ACCESS_GUEST }, | ||||
|             navOptions, | ||||
|             admOptions, | ||||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -547,7 +557,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|      * @param courseId Course ID. | ||||
|      * @returns Promise resolved when done. | ||||
|      */ | ||||
|     async invalidateCourseHandlers(courseId: number): Promise<void> { | ||||
|     protected async invalidateCourseHandlers(courseId: number): Promise<void> { | ||||
|         const promises: Promise<void>[] = []; | ||||
|         const courseData = this.coursesHandlers[courseId]; | ||||
| 
 | ||||
| @ -556,7 +566,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|         } | ||||
| 
 | ||||
|         courseData.enabledHandlers.forEach((handler) => { | ||||
|             if (handler?.invalidateEnabledForCourse) { | ||||
|             if (handler.invalidateEnabledForCourse) { | ||||
|                 promises.push( | ||||
|                     handler.invalidateEnabledForCourse(courseId, courseData.navOptions, courseData.admOptions), | ||||
|                 ); | ||||
| @ -579,7 +589,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|             return true; | ||||
|         } | ||||
| 
 | ||||
|         return time == this.lastUpdateHandlersForCoursesStart[courseId]; | ||||
|         return time === this.lastUpdateHandlersForCoursesStart[courseId]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -590,12 +600,13 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|      * @returns Promise resolved when done. | ||||
|      */ | ||||
|     protected async loadCourseOptions(course: CoreCourseAnyCourseDataWithOptions, refresh = false): Promise<void> { | ||||
|         if (course.navOptions === undefined || course.admOptions === undefined || refresh) { | ||||
| 
 | ||||
|             const options = await CoreCourses.getCoursesAdminAndNavOptions([course.id]); | ||||
|             course.navOptions = options.navOptions[course.id]; | ||||
|             course.admOptions = options.admOptions[course.id]; | ||||
|         if (!refresh && course.navOptions !== undefined && course.admOptions !== undefined) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const options = await CoreCourses.getCoursesAdminAndNavOptions([course.id]); | ||||
|         course.navOptions = options.navOptions[course.id]; | ||||
|         course.admOptions = options.admOptions[course.id]; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -618,7 +629,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
|      * @param admOptions Course admin options for current user. See CoreCoursesProvider.getUserAdministrationOptions. | ||||
|      * @returns Resolved when updated. | ||||
|      */ | ||||
|     async updateHandlersForCourse( | ||||
|     protected async updateHandlersForCourse( | ||||
|         courseId: number, | ||||
|         accessData: CoreCourseAccess, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
| @ -676,5 +687,5 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate<CoreCourseOpt | ||||
| export const CoreCourseOptionsDelegate = makeSingleton(CoreCourseOptionsDelegateService); | ||||
| 
 | ||||
| export type CoreCourseAccess = { | ||||
|     type: string; // Either CoreCourseProvider.ACCESS_GUEST or CoreCourseProvider.ACCESS_DEFAULT.
 | ||||
|     type: CoreCourseAccessDataType; | ||||
| }; | ||||
|  | ||||
| @ -80,7 +80,7 @@ declare module '@singletons/events' { | ||||
| /** | ||||
|  * Course Module completion status enumeration. | ||||
|  */ | ||||
| export enum CoreCourseModuleCompletionStatus { | ||||
| export const enum CoreCourseModuleCompletionStatus { | ||||
|     COMPLETION_INCOMPLETE = 0, | ||||
|     COMPLETION_COMPLETE = 1, | ||||
|     COMPLETION_COMPLETE_PASS = 2, | ||||
| @ -90,7 +90,7 @@ export enum CoreCourseModuleCompletionStatus { | ||||
| /** | ||||
|  * @deprecated since 4.3 Not used anymore. | ||||
|  */ | ||||
| export enum CoreCourseCompletionMode { | ||||
| export const enum CoreCourseCompletionMode { | ||||
|     FULL = 'full', | ||||
|     BASIC = 'basic', | ||||
| } | ||||
| @ -98,12 +98,20 @@ export enum CoreCourseCompletionMode { | ||||
| /** | ||||
|  * Completion tracking valid values. | ||||
|  */ | ||||
| export enum CoreCourseModuleCompletionTracking { | ||||
| export const enum CoreCourseModuleCompletionTracking { | ||||
|     COMPLETION_TRACKING_NONE = 0, | ||||
|     COMPLETION_TRACKING_MANUAL = 1, | ||||
|     COMPLETION_TRACKING_AUTOMATIC = 2, | ||||
| } | ||||
| 
 | ||||
| export const CoreCourseAccessDataType = { | ||||
|     ACCESS_GUEST: 'courses_access_guest', // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
|     ACCESS_DEFAULT: 'courses_access_default', // eslint-disable-line @typescript-eslint/naming-convention
 | ||||
| } as const; | ||||
| 
 | ||||
| // eslint-disable-next-line @typescript-eslint/no-redeclare
 | ||||
| export type CoreCourseAccessDataType = typeof CoreCourseAccessDataType[keyof typeof CoreCourseAccessDataType]; | ||||
| 
 | ||||
| /** | ||||
|  * Service that provides some features regarding a course. | ||||
|  */ | ||||
| @ -112,10 +120,17 @@ export class CoreCourseProvider { | ||||
| 
 | ||||
|     static readonly ALL_SECTIONS_ID = -2; | ||||
|     static readonly STEALTH_MODULES_SECTION_ID = -1; | ||||
|     static readonly ACCESS_GUEST = 'courses_access_guest'; | ||||
|     static readonly ACCESS_DEFAULT = 'courses_access_default'; | ||||
|     static readonly ALL_COURSES_CLEARED = -1; | ||||
| 
 | ||||
|     /** | ||||
|      * @deprecated since 4.4 Not used anymore. Use CoreCourseAccessDataType instead. | ||||
|      */ | ||||
|     static readonly ACCESS_GUEST = CoreCourseAccessDataType.ACCESS_GUEST; | ||||
|     /** | ||||
|      * @deprecated since 4.4 Not used anymore. Use CoreCourseAccessDataType instead. | ||||
|      */ | ||||
|     static readonly ACCESS_DEFAULT = CoreCourseAccessDataType.ACCESS_DEFAULT; | ||||
| 
 | ||||
|     static readonly COMPONENT = 'CoreCourse'; | ||||
| 
 | ||||
|     readonly CORE_MODULES = [ | ||||
|  | ||||
| @ -214,7 +214,7 @@ export class CoreCourseModulePrefetchDelegateService extends CoreDelegate<CoreCo | ||||
|      */ | ||||
|     async getCourseUpdates(modules: CoreCourseModuleData[], courseId: number): Promise<CourseUpdates> { | ||||
|         // Check if there's already a getCourseUpdates in progress.
 | ||||
|         const id = <string> Md5.hashAsciiStr(courseId + '#' + JSON.stringify(modules)); | ||||
|         const id = Md5.hashAsciiStr(courseId + '#' + JSON.stringify(modules)); | ||||
|         const siteId = CoreSites.getCurrentSiteId(); | ||||
| 
 | ||||
|         if (this.courseUpdatesPromises[siteId] && this.courseUpdatesPromises[siteId][id] !== undefined) { | ||||
|  | ||||
| @ -101,18 +101,9 @@ Feature: Test basic usage of one course in app | ||||
|     And I should not find "Test glossary" in the app | ||||
| 
 | ||||
|   Scenario: Guest access | ||||
|     Given I entered the course "Course 1" as "teacher1" in the app | ||||
|     And I press "Course summary" in the app | ||||
|     And I press "Open in browser" in the app | ||||
|     And I switch to the browser tab opened by the app | ||||
|     And I log in as "teacher1" | ||||
|     And I press "Actions menu" | ||||
|     And I follow "More..." | ||||
|     And I follow "Users" | ||||
|     And I follow "Enrolment methods" | ||||
|     Given I am on the "Course 1" "enrolment methods" page logged in as "teacher1" | ||||
|     And I click on "Enable" "icon" in the "Guest access" "table_row" | ||||
|     And I close the browser tab opened by the app | ||||
|     Given I entered the app as "student2" | ||||
|     And I entered the app as "student2" | ||||
|     When I press "Site home" in the app | ||||
|     And I press "Available courses" in the app | ||||
|     And I press "Course 1" in the app | ||||
|  | ||||
| @ -23,8 +23,7 @@ Feature: Test basic usage of guest access course in app | ||||
| 
 | ||||
|   @lms_from4.0 | ||||
|   Scenario: Guest access without password (student) | ||||
|     Given I log in as "teacher1" | ||||
|     And I am on the "Course 1" "enrolment methods" page | ||||
|     Given I am on the "Course 1" "enrolment methods" page logged in as "teacher1" | ||||
|     And I click on "Edit" "link" in the "Guest access" "table_row" | ||||
|     And I set the following fields to these values: | ||||
|       | Allow guest access | Yes | | ||||
| @ -47,8 +46,7 @@ Feature: Test basic usage of guest access course in app | ||||
| 
 | ||||
|   @lms_from4.3 | ||||
|   Scenario: Guest access with password (student) | ||||
|     Given I log in as "teacher1" | ||||
|     And I am on the "Course 1" "enrolment methods" page | ||||
|     Given I am on the "Course 1" "enrolment methods" page logged in as "teacher1" | ||||
|     And I click on "Edit" "link" in the "Guest access" "table_row" | ||||
|     And I set the following fields to these values: | ||||
|       | Allow guest access | Yes | | ||||
|  | ||||
| @ -27,8 +27,6 @@ import { AddonEnrolSelf } from '@addons/enrol/self/services/self'; | ||||
| import { CoreEnrol, CoreEnrolEnrolmentInfo, CoreEnrolEnrolmentMethod } from '@features/enrol/services/enrol'; | ||||
| import { CoreSiteWSPreSets, WSObservable } from '@classes/sites/authenticated-site'; | ||||
| 
 | ||||
| const ROOT_CACHE_KEY = 'mmCourses:'; | ||||
| 
 | ||||
| declare module '@singletons/events' { | ||||
| 
 | ||||
|     /** | ||||
| @ -50,6 +48,8 @@ declare module '@singletons/events' { | ||||
| @Injectable({ providedIn: 'root' }) | ||||
| export class CoreCoursesProvider { | ||||
| 
 | ||||
|     protected static readonly ROOT_CACHE_KEY = 'mmCourses:'; | ||||
| 
 | ||||
|     static readonly SEARCH_PER_PAGE = 20; | ||||
|     static readonly RECENT_PER_PAGE = 10; | ||||
|     static readonly ENROL_INVALID_KEY = 'CoreCoursesEnrolInvalidKey'; | ||||
| @ -114,7 +114,7 @@ export class CoreCoursesProvider { | ||||
|      * @returns Cache key. | ||||
|      */ | ||||
|     protected getCategoriesCacheKey(categoryId: number, addSubcategories?: boolean): string { | ||||
|         return ROOT_CACHE_KEY + 'categories:' + categoryId + ':' + !!addSubcategories; | ||||
|         return `${CoreCoursesProvider.ROOT_CACHE_KEY}categories:${categoryId}:${!!addSubcategories}`; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -131,16 +131,16 @@ export class CoreCoursesProvider { | ||||
|         if (courseIds.length == 1) { | ||||
|             // Only 1 course, check if it belongs to the user courses. If so, use all user courses.
 | ||||
|             return this.getCourseIdsIfEnrolled(courseIds[0], siteId); | ||||
|         } else { | ||||
|             if (courseIds.length > 1 && courseIds.indexOf(siteHomeId) == -1) { | ||||
|                 courseIds.push(siteHomeId); | ||||
|             } | ||||
| 
 | ||||
|             // Sort the course IDs.
 | ||||
|             courseIds.sort((a, b) => b - a); | ||||
| 
 | ||||
|             return courseIds; | ||||
|         } | ||||
| 
 | ||||
|         if (courseIds.length > 1 && courseIds.indexOf(siteHomeId) == -1) { | ||||
|             courseIds.push(siteHomeId); | ||||
|         } | ||||
| 
 | ||||
|         // Sort the course IDs.
 | ||||
|         courseIds.sort((a, b) => b - a); | ||||
| 
 | ||||
|         return courseIds; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -363,7 +363,7 @@ export class CoreCoursesProvider { | ||||
|      * @returns Cache key. | ||||
|      */ | ||||
|     protected getCoursesCacheKey(ids: number[]): string { | ||||
|         return ROOT_CACHE_KEY + 'course:' + JSON.stringify(ids); | ||||
|         return `${CoreCoursesProvider.ROOT_CACHE_KEY}course:${JSON.stringify(ids)}`; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -536,7 +536,7 @@ export class CoreCoursesProvider { | ||||
|      * @returns Cache key. | ||||
|      */ | ||||
|     protected getCoursesByFieldCacheKey(field: string = '', value: string | number = ''): string { | ||||
|         return ROOT_CACHE_KEY + 'coursesbyfield:' + field + ':' + value; | ||||
|         return `${CoreCoursesProvider.ROOT_CACHE_KEY}coursesbyfield:${field}:${value}`; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -651,7 +651,7 @@ export class CoreCoursesProvider { | ||||
|      * @returns Cache key. | ||||
|      */ | ||||
|     protected getRecentCoursesCacheKey(userId: number): string { | ||||
|         return `${ROOT_CACHE_KEY}:recentcourses:${userId}`; | ||||
|         return `${CoreCoursesProvider.ROOT_CACHE_KEY}:recentcourses:${userId}`; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -684,7 +684,7 @@ export class CoreCoursesProvider { | ||||
|      * @returns Cache key. | ||||
|      */ | ||||
|     protected getUserAdministrationOptionsCommonCacheKey(): string { | ||||
|         return ROOT_CACHE_KEY + 'administrationOptions:'; | ||||
|         return `${CoreCoursesProvider.ROOT_CACHE_KEY}administrationOptions:`; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -701,11 +701,14 @@ export class CoreCoursesProvider { | ||||
|      * Get user administration options for a set of courses. | ||||
|      * | ||||
|      * @param courseIds IDs of courses to get. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param options Options. | ||||
|      * @returns Promise resolved with administration options for each course. | ||||
|      */ | ||||
|     getUserAdministrationOptions(courseIds: number[], siteId?: string): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> { | ||||
|         return firstValueFrom(this.getUserAdministrationOptionsObservable(courseIds, { siteId })); | ||||
|     getUserAdministrationOptions( | ||||
|         courseIds: number[], | ||||
|         options?: CoreSitesCommonWSOptions, | ||||
|     ): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> { | ||||
|         return firstValueFrom(this.getUserAdministrationOptionsObservable(courseIds, options)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -752,7 +755,7 @@ export class CoreCoursesProvider { | ||||
|      * @returns Cache key. | ||||
|      */ | ||||
|     protected getUserNavigationOptionsCommonCacheKey(): string { | ||||
|         return ROOT_CACHE_KEY + 'navigationOptions:'; | ||||
|         return `${CoreCoursesProvider.ROOT_CACHE_KEY}navigationOptions:`; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -768,11 +771,14 @@ export class CoreCoursesProvider { | ||||
|      * Get user navigation options for a set of courses. | ||||
|      * | ||||
|      * @param courseIds IDs of courses to get. | ||||
|      * @param siteId Site ID. If not defined, current site. | ||||
|      * @param options Options. | ||||
|      * @returns Promise resolved with navigation options for each course. | ||||
|      */ | ||||
|     async getUserNavigationOptions(courseIds: number[], siteId?: string): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> { | ||||
|         return firstValueFrom(this.getUserNavigationOptionsObservable(courseIds, { siteId })); | ||||
|     getUserNavigationOptions( | ||||
|         courseIds: number[], | ||||
|         options?: CoreSitesCommonWSOptions, | ||||
|     ): Promise<CoreCourseUserAdminOrNavOptionCourseIndexed> { | ||||
|         return firstValueFrom(this.getUserNavigationOptionsObservable(courseIds, options)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -981,7 +987,7 @@ export class CoreCoursesProvider { | ||||
|      * @returns Cache key. | ||||
|      */ | ||||
|     protected getUserCoursesCacheKey(): string { | ||||
|         return ROOT_CACHE_KEY + 'usercourses'; | ||||
|         return `${CoreCoursesProvider.ROOT_CACHE_KEY}usercourses`; | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -22,7 +22,7 @@ import { | ||||
|     CoreCourseSearchedData, | ||||
|     CoreCourseUserAdminOrNavOptionIndexed, | ||||
| } from '@features/courses/services/courses'; | ||||
| import { CoreCourse, CoreCourseProvider } from '@features/course/services/course'; | ||||
| import { CoreCourse, CoreCourseAccessDataType } from '@features/course/services/course'; | ||||
| import { | ||||
|     CoreGrades, | ||||
|     CoreGradesGradeItem, | ||||
| @ -680,11 +680,11 @@ export class CoreGradesHelperProvider { | ||||
|         accessData: CoreCourseAccess, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): Promise<boolean> { | ||||
|         if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { | ||||
|         if (accessData && accessData.type === CoreCourseAccessDataType.ACCESS_GUEST) { | ||||
|             return false; // Not enabled for guests.
 | ||||
|         } | ||||
| 
 | ||||
|         if (navOptions && navOptions.grades !== undefined) { | ||||
|         if (navOptions?.grades !== undefined) { | ||||
|             return navOptions.grades; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -38,7 +38,7 @@ export class CoreGradesCourseOptionHandlerService implements CoreCourseOptionsHa | ||||
|     async invalidateEnabledForCourse(courseId: number, navOptions?: CoreCourseUserAdminOrNavOptionIndexed): Promise<void> { | ||||
|         await CoreGrades.invalidateCourseGradesPermissionsData(courseId); | ||||
| 
 | ||||
|         if (navOptions && navOptions.grades !== undefined) { | ||||
|         if (navOptions?.grades !== undefined) { | ||||
|             // No need to invalidate user courses.
 | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @ -38,7 +38,7 @@ export class CoreGradesCourseParticipantsOptionHandlerService implements CoreCou | ||||
|     async invalidateEnabledForCourse(courseId: number, navOptions?: CoreCourseUserAdminOrNavOptionIndexed): Promise<void> { | ||||
|         await CoreGrades.invalidateCourseGradesPermissionsData(courseId); | ||||
| 
 | ||||
|         if (navOptions && navOptions.grades !== undefined) { | ||||
|         if (navOptions?.grades !== undefined) { | ||||
|             // No need to invalidate user courses.
 | ||||
|             return; | ||||
|         } | ||||
|  | ||||
| @ -140,7 +140,7 @@ export class CoreH5PCore { | ||||
|         toHash.sort((a, b) => a.localeCompare(b)); | ||||
| 
 | ||||
|         // Calculate hash.
 | ||||
|         return <string> Md5.hashAsciiStr(toHash.join('')); | ||||
|         return Md5.hashAsciiStr(toHash.join('')); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1104,14 +1104,14 @@ export class CoreLoginHelperProvider { | ||||
| 
 | ||||
|         // Validate the signature.
 | ||||
|         // We need to check both http and https.
 | ||||
|         let signature = <string> Md5.hashAsciiStr(launchSiteURL + passport); | ||||
|         let signature = Md5.hashAsciiStr(launchSiteURL + passport); | ||||
|         if (signature != params[0]) { | ||||
|             if (launchSiteURL.indexOf('https://') != -1) { | ||||
|                 launchSiteURL = launchSiteURL.replace('https://', 'http://'); | ||||
|             } else { | ||||
|                 launchSiteURL = launchSiteURL.replace('http://', 'https://'); | ||||
|             } | ||||
|             signature = <string> Md5.hashAsciiStr(launchSiteURL + passport); | ||||
|             signature = Md5.hashAsciiStr(launchSiteURL + passport); | ||||
|         } | ||||
| 
 | ||||
|         if (signature == params[0]) { | ||||
|  | ||||
| @ -123,7 +123,7 @@ export class CoreSharedFilesListComponent implements OnInit, OnDestroy { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         const hash = <string> Md5.hashAsciiStr(path); | ||||
|         const hash = Md5.hashAsciiStr(path); | ||||
| 
 | ||||
|         CoreNavigator.navigate(`../${hash}`, { | ||||
|             params: { | ||||
|  | ||||
| @ -151,7 +151,7 @@ export class CoreSharedFilesProvider { | ||||
|      * @returns File ID. | ||||
|      */ | ||||
|     protected getFileId(entry: FileEntry): string { | ||||
|         return <string> Md5.hashAsciiStr(entry.name); | ||||
|         return Md5.hashAsciiStr(entry.name); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -87,7 +87,7 @@ export class CoreSitePluginsCourseOptionHandler extends CoreSitePluginsBaseHandl | ||||
|         const args = { | ||||
|             courseid: course.id, | ||||
|         }; | ||||
|         const hash = <string> Md5.hashAsciiStr(JSON.stringify(args)); | ||||
|         const hash = Md5.hashAsciiStr(JSON.stringify(args)); | ||||
| 
 | ||||
|         return { | ||||
|             title: this.title, | ||||
|  | ||||
| @ -24,13 +24,13 @@ import { | ||||
|     CoreSitePluginsContent, | ||||
|     CoreSitePluginsCourseModuleHandlerData, | ||||
|     CoreSitePluginsPlugin, | ||||
|     CoreSitePluginsProvider, | ||||
| } from '@features/siteplugins/services/siteplugins'; | ||||
| import { CoreNavigationOptions, CoreNavigator } from '@services/navigator'; | ||||
| import { CoreLogger } from '@singletons/logger'; | ||||
| import { CoreSitePluginsBaseHandler } from './base-handler'; | ||||
| import { CoreEvents } from '@singletons/events'; | ||||
| import { CoreUtils } from '@services/utils/utils'; | ||||
| import { CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT } from '@features/siteplugins/constants'; | ||||
| 
 | ||||
| /** | ||||
|  * Handler to support a module using a site plugin. | ||||
| @ -114,7 +114,7 @@ export class CoreSitePluginsModuleHandler extends CoreSitePluginsBaseHandler imp | ||||
|             this.loadCoursePageTemplate(module, courseId, handlerData, method); | ||||
| 
 | ||||
|             // Allow updating the data via event.
 | ||||
|             CoreEvents.on(CoreSitePluginsProvider.UPDATE_COURSE_CONTENT, (data) => { | ||||
|             CoreEvents.on(CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT, (data) => { | ||||
|                 if (data.cmId === module.id) { | ||||
|                     this.loadCoursePageTemplate(module, courseId, handlerData, method, !data.alreadyFetched); | ||||
|                 } | ||||
|  | ||||
| @ -98,7 +98,7 @@ export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandle | ||||
|                     courseid: contextId, | ||||
|                     userid: user.id, | ||||
|                 }; | ||||
|                 const hash = <string> Md5.hashAsciiStr(JSON.stringify(args)); | ||||
|                 const hash = Md5.hashAsciiStr(JSON.stringify(args)); | ||||
| 
 | ||||
|                 CoreNavigator.navigateToSitePath( | ||||
|                     `siteplugins/content/${this.plugin.component}/${this.handlerSchema.method}/${hash}`, | ||||
|  | ||||
| @ -58,7 +58,7 @@ export class CoreSitePluginsOnlyTitleBlockComponent extends CoreBlockBaseCompone | ||||
|             contextlevel: this.contextLevel, | ||||
|             instanceid: this.instanceId, | ||||
|         }; | ||||
|         const hash = <string> Md5.hashAsciiStr(JSON.stringify(args)); | ||||
|         const hash = Md5.hashAsciiStr(JSON.stringify(args)); | ||||
| 
 | ||||
|         CoreNavigator.navigateToSitePath( | ||||
|             `siteplugins/content/${handler.plugin.component}/${handler.handlerSchema.method}/${hash}`, | ||||
|  | ||||
| @ -29,11 +29,12 @@ import { Md5 } from 'ts-md5'; | ||||
| 
 | ||||
| import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; | ||||
| import { CoreCompileHtmlComponent } from '@features/compile/components/compile-html/compile-html'; | ||||
| import { CoreSitePlugins, CoreSitePluginsContent, CoreSitePluginsProvider } from '@features/siteplugins/services/siteplugins'; | ||||
| import { CoreSitePlugins, CoreSitePluginsContent } from '@features/siteplugins/services/siteplugins'; | ||||
| import { CoreNavigator } from '@services/navigator'; | ||||
| import { CoreDomUtils } from '@services/utils/dom'; | ||||
| import { CoreEvents } from '@singletons/events'; | ||||
| import { CoreSites, CoreSitesReadingStrategy } from '@services/sites'; | ||||
| import { CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT } from '@features/siteplugins/constants'; | ||||
| 
 | ||||
| /** | ||||
|  * Component to render a site plugin content. | ||||
| @ -181,7 +182,7 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { | ||||
| 
 | ||||
|         component = component || this.component; | ||||
|         method = method || this.method; | ||||
|         const hash = <string> Md5.hashAsciiStr(JSON.stringify(args)); | ||||
|         const hash = Md5.hashAsciiStr(JSON.stringify(args)); | ||||
| 
 | ||||
|         CoreNavigator.navigateToSitePath(`siteplugins/content/${component}/${method}/${hash}`, { | ||||
|             params: { | ||||
| @ -261,7 +262,7 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck { | ||||
|      * @param alreadyFetched Whether course data has already been fetched (no need to fetch it again). | ||||
|      */ | ||||
|     updateModuleCourseContent(cmId: number, alreadyFetched?: boolean): void { | ||||
|         CoreEvents.trigger(CoreSitePluginsProvider.UPDATE_COURSE_CONTENT, { cmId, alreadyFetched }); | ||||
|         CoreEvents.trigger(CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT, { cmId, alreadyFetched }); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
							
								
								
									
										15
									
								
								src/core/features/siteplugins/constants.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/core/features/siteplugins/constants.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,15 @@ | ||||
| // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||
| //
 | ||||
| // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||
| // you may not use this file except in compliance with the License.
 | ||||
| // You may obtain a copy of the License at
 | ||||
| //
 | ||||
| //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||
| //
 | ||||
| // Unless required by applicable law or agreed to in writing, software
 | ||||
| // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||
| // See the License for the specific language governing permissions and
 | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| export const CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT = 'siteplugins_update_course_content'; | ||||
| @ -99,7 +99,7 @@ export class CoreSitePluginsCallWSNewContentDirective extends CoreSitePluginsCal | ||||
|         } else { | ||||
|             const component = this.component || this.parentContent?.component; | ||||
|             const method = this.method || this.parentContent?.method; | ||||
|             const hash = <string> Md5.hashAsciiStr(JSON.stringify(args)); | ||||
|             const hash = Md5.hashAsciiStr(JSON.stringify(args)); | ||||
| 
 | ||||
|             CoreNavigator.navigateToSitePath(`siteplugins/content/${component}/${method}/${hash}`, { | ||||
|                 params: { | ||||
|  | ||||
| @ -98,7 +98,7 @@ export class CoreSitePluginsNewContentDirective implements OnInit { | ||||
|             } else { | ||||
|                 const component = this.component || this.parentContent?.component; | ||||
|                 const method = this.method || this.parentContent?.method; | ||||
|                 const hash = <string> Md5.hashAsciiStr(JSON.stringify(args)); | ||||
|                 const hash = Md5.hashAsciiStr(JSON.stringify(args)); | ||||
| 
 | ||||
|                 CoreNavigator.navigateToSitePath(`siteplugins/content/${component}/${method}/${hash}`, { | ||||
|                     params: { | ||||
|  | ||||
| @ -268,13 +268,13 @@ export class CoreSitePluginsHelperProvider { | ||||
|         } | ||||
| 
 | ||||
|         // Create a "fake" instance to hold all the libraries.
 | ||||
|         const lazyLibraries = await CoreCompile.getLazyLibraries(); | ||||
|         const instance = { | ||||
|             // eslint-disable-next-line @typescript-eslint/naming-convention
 | ||||
|             HANDLER_DISABLED: HANDLER_DISABLED, | ||||
|         }; | ||||
| 
 | ||||
|         CoreCompile.injectLibraries(instance, lazyLibraries); | ||||
|         await CoreCompile.loadLibraries(); | ||||
|         CoreCompile.injectLibraries(instance); | ||||
| 
 | ||||
|         // Add some data of the WS call result.
 | ||||
|         const jsData = CoreSitePlugins.createDataForJS(result); | ||||
|  | ||||
| @ -34,6 +34,7 @@ import { CorePlatform } from '@services/platform'; | ||||
| import { CoreEnrolAction, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate'; | ||||
| import { CoreSiteWSPreSets } from '@classes/sites/authenticated-site'; | ||||
| import { CoreUserProfileHandlerType } from '@features/user/services/user-delegate'; | ||||
| import { CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT } from '../constants'; | ||||
| 
 | ||||
| const ROOT_CACHE_KEY = 'CoreSitePlugins:'; | ||||
| 
 | ||||
| @ -44,7 +45,7 @@ const ROOT_CACHE_KEY = 'CoreSitePlugins:'; | ||||
| export class CoreSitePluginsProvider { | ||||
| 
 | ||||
|     static readonly COMPONENT = 'CoreSitePlugins'; | ||||
|     static readonly UPDATE_COURSE_CONTENT = 'siteplugins_update_course_content'; | ||||
|     static readonly UPDATE_COURSE_CONTENT = CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT; | ||||
| 
 | ||||
|     protected logger: CoreLogger; | ||||
|     protected sitePlugins: {[name: string]: CoreSitePluginsHandler} = {}; // Site plugins registered.
 | ||||
| @ -995,7 +996,7 @@ declare module '@singletons/events' { | ||||
|      * @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
 | ||||
|      */ | ||||
|     export interface CoreEventsData { | ||||
|         [CoreSitePluginsProvider.UPDATE_COURSE_CONTENT]: CoreSitePluginsUpdateCourseContentEvent; | ||||
|         [CORE_SITE_PLUGINS_UPDATE_COURSE_CONTENT]: CoreSitePluginsUpdateCourseContentEvent; | ||||
|     } | ||||
| 
 | ||||
| } | ||||
|  | ||||
| @ -27,6 +27,42 @@ import { canLeaveGuard } from '@guards/can-leave'; | ||||
| import { CoreSitePluginsCourseOptionPage } from '@features/siteplugins/pages/course-option/course-option'; | ||||
| import { CoreSitePluginsModuleIndexPage } from '@features/siteplugins/pages/module-index/module-index'; | ||||
| 
 | ||||
| /** | ||||
|  * Get site plugins exported objects. | ||||
|  * | ||||
|  * @returns Site plugins exported objects. | ||||
|  */ | ||||
| export async function getSitePluginsExportedObjects(): Promise<Record<string, unknown>> { | ||||
|     const { CoreSitePluginsModuleIndexComponent } = await import ('@features/siteplugins/components/module-index/module-index'); | ||||
|     const { CoreSitePluginsBlockComponent } = await import ('@features/siteplugins/components/block/block'); | ||||
|     const { CoreSitePluginsCourseFormatComponent } = await import ('@features/siteplugins/components/course-format/course-format'); | ||||
|     const { CoreSitePluginsQuestionComponent } = await import ('@features/siteplugins/components/question/question'); | ||||
|     const { CoreSitePluginsQuestionBehaviourComponent } | ||||
|         = await import ('@features/siteplugins/components/question-behaviour/question-behaviour'); | ||||
|     const { CoreSitePluginsUserProfileFieldComponent } | ||||
|         = await import ('@features/siteplugins/components/user-profile-field/user-profile-field'); | ||||
|     const { CoreSitePluginsQuizAccessRuleComponent } | ||||
|         = await import ('@features/siteplugins/components/quiz-access-rule/quiz-access-rule'); | ||||
|     const { CoreSitePluginsAssignFeedbackComponent } | ||||
|         = await import ('@features/siteplugins/components/assign-feedback/assign-feedback'); | ||||
|     const { CoreSitePluginsAssignSubmissionComponent } | ||||
|         = await import ('@features/siteplugins/components/assign-submission/assign-submission'); | ||||
| 
 | ||||
|     /* eslint-disable @typescript-eslint/naming-convention */ | ||||
|     return { | ||||
|         CoreSitePluginsModuleIndexComponent, | ||||
|         CoreSitePluginsBlockComponent, | ||||
|         CoreSitePluginsCourseFormatComponent, | ||||
|         CoreSitePluginsQuestionComponent, | ||||
|         CoreSitePluginsQuestionBehaviourComponent, | ||||
|         CoreSitePluginsUserProfileFieldComponent, | ||||
|         CoreSitePluginsQuizAccessRuleComponent, | ||||
|         CoreSitePluginsAssignFeedbackComponent, | ||||
|         CoreSitePluginsAssignSubmissionComponent, | ||||
|     }; | ||||
|     /* eslint-enable @typescript-eslint/naming-convention */ | ||||
| } | ||||
| 
 | ||||
| const routes: Routes = [ | ||||
|     { | ||||
|         path: 'siteplugins/content/:component/:method/:hash', | ||||
|  | ||||
| @ -218,7 +218,7 @@ export class CoreStylesService { | ||||
|             contents = (await handler.getStyle(siteId, config)).trim(); | ||||
|         } | ||||
| 
 | ||||
|         const hash = <string>Md5.hashAsciiStr(contents); | ||||
|         const hash = Md5.hashAsciiStr(contents); | ||||
| 
 | ||||
|         // Update the styles only if they have changed.
 | ||||
|         if (this.stylesEls[siteId][handler.name] === hash) { | ||||
|  | ||||
| @ -13,7 +13,7 @@ | ||||
| // limitations under the License.
 | ||||
| 
 | ||||
| import { Injectable } from '@angular/core'; | ||||
| import { CoreCourseProvider } from '@features/course/services/course'; | ||||
| import { CoreCourseAccessDataType } from '@features/course/services/course'; | ||||
| import { | ||||
|     CoreCourseAccess, | ||||
|     CoreCourseOptionsHandler, | ||||
| @ -37,7 +37,7 @@ export class CoreUserCourseOptionHandlerService implements CoreCourseOptionsHand | ||||
|      * @inheritdoc | ||||
|      */ | ||||
|     invalidateEnabledForCourse(courseId: number, navOptions?: CoreCourseUserAdminOrNavOptionIndexed): Promise<void> { | ||||
|         if (navOptions && navOptions.participants !== undefined) { | ||||
|         if (navOptions?.participants !== undefined) { | ||||
|             // No need to invalidate anything.
 | ||||
|             return Promise.resolve(); | ||||
|         } | ||||
| @ -60,11 +60,11 @@ export class CoreUserCourseOptionHandlerService implements CoreCourseOptionsHand | ||||
|         accessData: CoreCourseAccess, | ||||
|         navOptions?: CoreCourseUserAdminOrNavOptionIndexed, | ||||
|     ): boolean | Promise<boolean> { | ||||
|         if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { | ||||
|         if (accessData && accessData.type === CoreCourseAccessDataType.ACCESS_GUEST) { | ||||
|             return false; // Not enabled for guests.
 | ||||
|         } | ||||
| 
 | ||||
|         if (navOptions && navOptions.participants !== undefined) { | ||||
|         if (navOptions?.participants !== undefined) { | ||||
|             return navOptions.participants; | ||||
|         } | ||||
| 
 | ||||
|  | ||||
| @ -1303,7 +1303,7 @@ export class CoreFilepoolProvider { | ||||
|      * @returns File download ID. | ||||
|      */ | ||||
|     protected getFileDownloadId(fileUrl: string, filePath: string): string { | ||||
|         return <string> Md5.hashAsciiStr(fileUrl + '###' + filePath); | ||||
|         return Md5.hashAsciiStr(fileUrl + '###' + filePath); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
| @ -1867,7 +1867,7 @@ export class CoreFilepoolProvider { | ||||
|      * @returns Package ID. | ||||
|      */ | ||||
|     getPackageId(component: string, componentId?: string | number): string { | ||||
|         return <string> Md5.hashAsciiStr(component + '#' + this.fixComponentId(componentId)); | ||||
|         return Md5.hashAsciiStr(component + '#' + this.fixComponentId(componentId)); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -721,7 +721,7 @@ export class CoreSitesProvider { | ||||
|      * @returns Site ID. | ||||
|      */ | ||||
|     createSiteID(siteUrl: string, username: string): string { | ||||
|         return <string> Md5.hashAsciiStr(siteUrl + username); | ||||
|         return Md5.hashAsciiStr(siteUrl + username); | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|  | ||||
| @ -1456,7 +1456,7 @@ export class CoreDomUtilsProvider { | ||||
|         const listenCloseEvents = closeOnNavigate ?? true; // Default to true.
 | ||||
| 
 | ||||
|         // TODO: Improve this if we need two modals with same component open at the same time.
 | ||||
|         const modalId = <string> Md5.hashAsciiStr(options.component?.toString() || ''); | ||||
|         const modalId = Md5.hashAsciiStr(options.component?.toString() || ''); | ||||
| 
 | ||||
|         const modal = this.displayedModals[modalId] | ||||
|             ? this.displayedModals[modalId] | ||||
|  | ||||
| @ -4,22 +4,22 @@ Feature: It opens external links properly. | ||||
|   Background: | ||||
|     Given the following "users" exist: | ||||
|       | username | | ||||
|       | student1 | | ||||
|       | teacher1 | | ||||
|     And the following "courses" exist: | ||||
|       | fullname | shortname | | ||||
|       | Course 1 | C1        | | ||||
|     And the following "course enrolments" exist: | ||||
|       | user     | course | role    | | ||||
|       | student1 | C1     | student | | ||||
|       | teacher1 | C1     | teacher | | ||||
|     And the following "activities" exist: | ||||
|       | activity   | name       | intro       | course | idnumber | | ||||
|       | forum      | Test forum | Test forum  | C1     | forum    | | ||||
|     And the following forum discussions exist in course "Course 1": | ||||
|       | forum      | user     | name        | message       | | ||||
|       | Test forum | student1 | Forum topic | See <a href="https://moodle.org/">moodle.org external link</a> | | ||||
|       | Test forum | teacher1 | Forum topic | See <a href="https://moodle.org/">moodle.org external link</a> | | ||||
| 
 | ||||
|   Scenario: Click an external link | ||||
|     Given I entered the forum activity "Test forum" on course "Course 1" as "student1" in the app | ||||
|     Given I entered the forum activity "Test forum" on course "Course 1" as "teacher1" in the app | ||||
|     When I press "Forum topic" in the app | ||||
|     And I press "moodle.org external link" in the app | ||||
|     Then I should find "You are about to leave the app" in the app | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user