Merge pull request #3990 from crazyserver/MOBILE-4543

Mobile 4543
main
Dani Palou 2024-04-04 15:52:10 +02:00 committed by GitHub
commit 3036d096aa
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
59 changed files with 446 additions and 286 deletions

View File

@ -64,7 +64,7 @@ export class AddonBadgesUserHandlerService implements CoreUserProfileHandler {
return false;
}
if (navOptions && navOptions.badges !== undefined) {
if (navOptions?.badges !== undefined) {
return navOptions.badges;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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.
}

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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}`,

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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(),
}));

View File

@ -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,

View File

@ -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;
}

View File

@ -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 });
}

View File

@ -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>[] = [
/* eslint-disable @typescript-eslint/naming-convention */
return {
CoreError,
CoreWSError,
CoreCanceledError,
CoreSilentError,
CoreAjaxError,
CoreAjaxWSError,
CoreCanceledError,
CoreCaptureError,
CoreError,
CoreNetworkError,
CoreSilentError,
CoreSiteError,
CoreLoginError,
CoreWSError,
CoreErrorWithOptions,
CoreHttpError,
];
};
/* eslint-enable @typescript-eslint/naming-convention */
}

View File

@ -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));
}
/**

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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,

View File

@ -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>

View File

@ -39,3 +39,7 @@ ion-item.card-header {
margin: 0px;
}
}
.core-module-oib-button.hidden {
display: none;
}

View File

@ -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);
}
/**

View File

@ -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}`;

View File

@ -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>

View File

@ -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;
}

View File

@ -104,4 +104,8 @@
display: inline;
}
}
.core-course-oib-button.hidden {
display: none;
}
}

View File

@ -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);

View File

@ -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,13 +600,14 @@ 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) {
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];
}
}
/**
* Update handlers for each course.
@ -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;
};

View File

@ -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 = [

View File

@ -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) {

View File

@ -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

View File

@ -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 |

View File

@ -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,7 +131,8 @@ 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);
}
@ -141,7 +142,6 @@ export class CoreCoursesProvider {
return courseIds;
}
}
/**
* Given a course ID, if user is enrolled in the course it will return the IDs of all enrolled courses and site home.
@ -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`;
}
/**

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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(''));
}
/**

View File

@ -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]) {

View File

@ -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: {

View File

@ -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);
}
/**

View File

@ -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,

View File

@ -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);
}

View File

@ -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}`,

View File

@ -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}`,

View File

@ -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 });
}
/**

View 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';

View File

@ -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: {

View File

@ -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: {

View File

@ -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);

View File

@ -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;
}
}

View File

@ -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',

View File

@ -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) {

View File

@ -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;
}

View File

@ -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));
}
/**

View File

@ -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);
}
/**

View File

@ -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]

View File

@ -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