Merge pull request #2675 from crazyserver/MOBILE-3650

Mobile 3650
main
Dani Palou 2021-02-09 16:39:47 +01:00 committed by GitHub
commit 9ba630c02a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1127 additions and 4 deletions

View File

@ -14,8 +14,8 @@ jobs:
with: with:
node-version: '12.x' node-version: '12.x'
- run: npm ci - run: npm ci
- run: result=$(find src -type f -iname '*.html' -exec sh -c 'cat {} | tr "\n" " " | grep -Eo "class=\"[^\"]+\"[^>]+class=\"" ' \; | wc -l); test $result -eq 0
- run: npm run lint - run: npm run lint
- run: npx tslint -c ionic-migration.json -p tsconfig.json - run: npx tslint -c ionic-migration.json -p tsconfig.json
- run: npm run test:ci - run: npm run test:ci
- run: npm run build:prod - run: npm run build:prod
- run: result=$(find src -type f -iname '*.html' -exec sh -c 'cat {} | tr "\n" " " | grep -Eo "class=\"[^\"]+\"[^>]+class=\"" ' \; | wc -l); test $result -eq 0

View File

@ -376,13 +376,13 @@ export class AddonModBookProvider {
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the WS call is successful. * @return Promise resolved when the WS call is successful.
*/ */
logView(id: number, chapterId?: number, name?: string, siteId?: string): Promise<void> { async logView(id: number, chapterId?: number, name?: string, siteId?: string): Promise<void> {
const params: AddonModBookViewBookWSParams = { const params: AddonModBookViewBookWSParams = {
bookid: id, bookid: id,
chapterid: chapterId, chapterid: chapterId,
}; };
return CoreCourseLogHelper.instance.logSingle( await CoreCourseLogHelper.instance.logSingle(
'mod_book_view_book', 'mod_book_view_book',
params, params,
AddonModBookProvider.COMPONENT, AddonModBookProvider.COMPONENT,

View File

@ -16,12 +16,14 @@ import { NgModule } from '@angular/core';
import { AddonModBookModule } from './book/book.module'; import { AddonModBookModule } from './book/book.module';
import { AddonModLessonModule } from './lesson/lesson.module'; import { AddonModLessonModule } from './lesson/lesson.module';
import { AddonModPageModule } from './page/page.module';
@NgModule({ @NgModule({
declarations: [], declarations: [],
imports: [ imports: [
AddonModBookModule, AddonModBookModule,
AddonModLessonModule, AddonModLessonModule,
AddonModPageModule,
], ],
providers: [], providers: [],
exports: [], exports: [],

View File

@ -0,0 +1,42 @@
// (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.
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreSharedModule } from '@/core/shared.module';
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
import { AddonModPageIndexComponent } from './index/index';
@NgModule({
declarations: [
AddonModPageIndexComponent,
],
imports: [
CommonModule,
IonicModule,
TranslateModule.forChild(),
FormsModule,
CoreSharedModule,
CoreCourseComponentsModule,
],
exports: [
AddonModPageIndexComponent,
],
})
export class AddonModPageComponentsModule {}

View File

@ -0,0 +1,47 @@
<!-- Buttons to add to the header. -->
<core-navbar-buttons slot="end">
<core-context-menu>
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate"
[href]="externalUrl" iconAction="fas-external-link-alt">
</core-context-menu-item>
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate"
(action)="expandDescription()" iconAction="fas-arrow-right">
</core-context-menu-item>
<core-context-menu-item *ngIf="blog" [priority]="750" content="{{'addon.blog.blog' | translate}}"
iconAction="far-newspaper" (action)="gotoBlog()">
</core-context-menu-item>
<core-context-menu-item [priority]="700" [content]="'core.refresh' | translate" (action)="doRefresh(null, $event)"
[iconAction]="refreshIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="600" [content]="prefetchText" (action)="prefetch($event)"
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
</core-context-menu-item>
<core-context-menu-item *ngIf="size" [priority]="500" [content]="'core.clearstoreddata' | translate:{$a: size}"
iconDescription="fas-archive" (action)="removeFiles($event)" iconAction="fas-trash" [closeOnClick]="false">
</core-context-menu-item>
</core-context-menu>
</core-navbar-buttons>
<!-- Content. -->
<core-loading [hideUntil]="loaded" class="core-loading-center safe-area-page">
<core-course-module-description *ngIf="displayDescription" [description]="description" [component]="component"
[componentId]="componentId" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
</core-course-module-description>
<ion-card class="core-warning-card" *ngIf="warning">
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
<span [innerHTML]="warning"></span>
</ion-card>
<div class="ion-padding">
<core-format-text [component]="component" [componentId]="componentId" [text]="contents" contextLevel="module"
[contextInstanceId]="module?.id" [courseId]="courseId">
</core-format-text>
<p class="ion-padding-bottom addon-mod_page-timemodified" *ngIf="displayTimemodified && timemodified">
{{ 'core.lastmodified' | translate}}: {{ timemodified! * 1000 | coreFormatDate }}
</p>
</div>
</core-loading>

View File

@ -0,0 +1,8 @@
/* Solves iframe height */
.core-loading-content > div[padding] {
height: 100%;
}
core-format-text > .no-overflow {
display: inline;
}

View File

@ -0,0 +1,150 @@
// (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.
import { Component, OnInit, Optional } from '@angular/core';
import {
CoreCourseModuleMainResourceComponent,
} from '@features/course/classes/main-resource-component';
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
import { CoreCourse, CoreCourseWSModule } from '@features/course/services/course';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import { AddonModPageProvider, AddonModPagePage, AddonModPage } from '../../services/page';
import { AddonModPageHelper } from '../../services/page-helper';
/**
* Component that displays a page.
*/
@Component({
selector: 'addon-mod-page-index',
templateUrl: 'addon-mod-page-index.html',
styleUrls: ['index.scss'],
})
export class AddonModPageIndexComponent extends CoreCourseModuleMainResourceComponent implements OnInit {
component = AddonModPageProvider.COMPONENT;
canGetPage = false;
contents?: string;
displayDescription = true;
displayTimemodified = true;
timemodified?: number;
page?: CoreCourseWSModule | AddonModPagePage;
warning?: string;
protected fetchContentDefaultError = 'addon.mod_page.errorwhileloadingthepage';
constructor(@Optional() courseContentsPage?: CoreCourseContentsPage) {
super('AddonModPageIndexComponent', courseContentsPage);
}
/**
* Component being initialized.
*/
async ngOnInit(): Promise<void> {
super.ngOnInit();
this.canGetPage = AddonModPage.instance.isGetPageWSAvailable();
await this.loadContent();
try {
await AddonModPage.instance.logView(this.module!.instance!, this.module!.name);
CoreCourse.instance.checkModuleCompletion(this.courseId!, this.module!.completiondata);
} catch {
// Ignore errors.
}
}
/**
* Perform the invalidate content function.
*
* @return Resolved when done.
*/
protected async invalidateContent(): Promise<void> {
await AddonModPage.instance.invalidateContent(this.module!.id, this.courseId!);
}
/**
* Download page contents.
*
* @param refresh Whether we're refreshing data.
* @return Promise resolved when done.
*/
protected async fetchContent(refresh?: boolean): Promise<void> {
// Download the resource if it needs to be downloaded.
try {
const downloadResult = await this.downloadResourceIfNeeded(refresh);
const promises: Promise<void>[] = [];
let getPagePromise: Promise<CoreCourseWSModule | AddonModPagePage>;
// Get the module to get the latest title and description. Data should've been updated in download.
if (this.canGetPage) {
getPagePromise = AddonModPage.instance.getPageData(this.courseId!, this.module!.id);
} else {
getPagePromise = CoreCourse.instance.getModule(this.module!.id, this.courseId!);
}
promises.push(getPagePromise.then((page) => {
if (!page) {
return;
}
this.description = 'intro' in page ? page.intro : page.description;
this.dataRetrieved.emit(page);
if (!this.canGetPage) {
return;
}
this.page = page;
// Check if description and timemodified should be displayed.
if ('displayoptions' in this.page) {
const options: Record<string, string | boolean> =
CoreTextUtils.instance.unserialize(this.page.displayoptions) || {};
this.displayDescription = typeof options.printintro == 'undefined' ||
CoreUtils.instance.isTrueOrOne(options.printintro);
this.displayTimemodified = typeof options.printlastmodified == 'undefined' ||
CoreUtils.instance.isTrueOrOne(options.printlastmodified);
} else {
this.displayDescription = true;
this.displayTimemodified = true;
}
this.timemodified = 'timemodified' in this.page ? this.page.timemodified : undefined;
return;
}).catch(() => {
// Ignore errors.
}));
// Get the page HTML.
promises.push(AddonModPageHelper.instance.getPageHtml(this.module!.contents, this.module!.id).then((content) => {
this.contents = content;
this.warning = downloadResult?.failed ? this.getErrorDownloadingSomeFilesMessage(downloadResult.error!) : '';
return;
}));
await Promise.all(promises);
} finally {
this.fillContextMenu(refresh);
}
}
}

View File

@ -0,0 +1,4 @@
{
"errorwhileloadingthepage": "Error while loading the page content.",
"modulenameplural": "Pages"
}

View File

@ -0,0 +1,28 @@
// (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.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: ':courseId/:cmdId',
loadChildren: () => import('./pages/index/index.module').then( m => m.AddonModPageIndexPageModule),
},
];
@NgModule({
imports: [RouterModule.forChild(routes)],
})
export class AddonModPageLazyModule {}

View File

@ -0,0 +1,56 @@
// (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.
import { APP_INITIALIZER, NgModule } from '@angular/core';
import { Routes } from '@angular/router';
import { CoreContentLinksDelegate } from '@features/contentlinks/services/contentlinks-delegate';
import { CoreCourseModuleDelegate } from '@features/course/services/module-delegate';
import { CoreCourseModulePrefetchDelegate } from '@features/course/services/module-prefetch-delegate';
import { CoreMainMenuTabRoutingModule } from '@features/mainmenu/mainmenu-tab-routing.module';
import { CorePluginFileDelegate } from '@services/plugin-file-delegate';
import { AddonModPageComponentsModule } from './components/components.module';
import { AddonModPageIndexLinkHandler } from './services/handlers/index-link';
import { AddonModPageListLinkHandler } from './services/handlers/list-link';
import { AddonModPageModuleHandler, AddonModPageModuleHandlerService } from './services/handlers/module';
import { AddonModPagePluginFileHandler } from './services/handlers/pluginfile';
import { AddonModPagePrefetchHandler } from './services/handlers/prefetch';
const routes: Routes = [
{
path: AddonModPageModuleHandlerService.PAGE_NAME,
loadChildren: () => import('./page-lazy.module').then(m => m.AddonModPageLazyModule),
},
];
@NgModule({
imports: [
CoreMainMenuTabRoutingModule.forChild(routes),
AddonModPageComponentsModule,
],
providers: [
{
provide: APP_INITIALIZER,
multi: true,
deps: [],
useFactory: () => () => {
CoreCourseModuleDelegate.instance.registerHandler(AddonModPageModuleHandler.instance);
CoreContentLinksDelegate.instance.registerHandler(AddonModPageIndexLinkHandler.instance);
CoreContentLinksDelegate.instance.registerHandler(AddonModPageListLinkHandler.instance);
CoreCourseModulePrefetchDelegate.instance.registerHandler(AddonModPagePrefetchHandler.instance);
CorePluginFileDelegate.instance.registerHandler(AddonModPagePluginFileHandler.instance);
},
},
],
})
export class AddonModPageModule {}

View File

@ -0,0 +1,22 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button [attr.aria-label]="'core.back' | translate"></ion-back-button>
</ion-buttons>
<ion-title>
<core-format-text [text]="title" contextLevel="module" [contextInstanceId]="module?.id" [courseId]="courseId">
</core-format-text>
</ion-title>
<ion-buttons slot="end">
<!-- The buttons defined by the component will be added in here. -->
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-refresher slot="fixed" [disabled]="!pageComponent?.loaded" (ionRefresh)="pageComponent?.doRefresh($event)">
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
</ion-refresher>
<addon-mod-page-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)">
</addon-mod-page-index>
</ion-content>

View File

@ -0,0 +1,46 @@
// (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.
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CommonModule } from '@angular/common';
import { IonicModule } from '@ionic/angular';
import { TranslateModule } from '@ngx-translate/core';
import { CoreSharedModule } from '@/core/shared.module';
import { AddonModPageComponentsModule } from '../../components/components.module';
import { AddonModPageIndexPage } from './index';
const routes: Routes = [
{
path: '',
component: AddonModPageIndexPage,
},
];
@NgModule({
imports: [
RouterModule.forChild(routes),
CommonModule,
IonicModule,
TranslateModule.forChild(),
CoreSharedModule,
AddonModPageComponentsModule,
],
declarations: [
AddonModPageIndexPage,
],
exports: [RouterModule],
})
export class AddonModPageIndexPageModule {}

View File

@ -0,0 +1,54 @@
// (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.
import { Component, OnInit, ViewChild } from '@angular/core';
import { CoreCourseWSModule } from '@features/course/services/course';
import { CoreNavigator } from '@services/navigator';
import { AddonModPageIndexComponent } from '../../components/index/index';
import { AddonModPagePage } from '../../services/page';
/**
* Page that displays a page.
*/
@Component({
selector: 'page-addon-mod-page-index',
templateUrl: 'index.html',
})
export class AddonModPageIndexPage implements OnInit {
@ViewChild(AddonModPageIndexComponent) pageComponent?: AddonModPageIndexComponent;
title?: string;
module?: CoreCourseWSModule;
courseId?: number;
/**
* Component being initialized.
*/
ngOnInit(): void {
this.module = CoreNavigator.instance.getRouteParam('module');
this.courseId = CoreNavigator.instance.getRouteNumberParam('courseId');
this.title = this.module?.name;
}
/**
* Update some data based on the page instance.
*
* @param page Page instance.
*/
updateData(page: CoreCourseWSModule | AddonModPagePage): void {
this.title = 'name' in page ? page.name : this.title;
}
}

View File

@ -0,0 +1,44 @@
// (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.
import { Injectable } from '@angular/core';
import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/classes/module-index-handler';
import { makeSingleton } from '@singletons';
import { AddonModPage } from '../page';
/**
* Handler to treat links to page resource.
*/
@Injectable({ providedIn: 'root' })
export class AddonModPageIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
name = 'AddonModPageLinkHandler';
constructor() {
super('AddonModPage', 'page', 'p');
}
/**
* Check if the handler is enabled for a certain site (site + user) and a URL.
*
* @param siteId The site ID.
* @return Whether the handler is enabled for the URL and site.
*/
isEnabled(siteId: string): Promise<boolean> {
return AddonModPage.instance.isPluginEnabled(siteId);
}
}
export class AddonModPageIndexLinkHandler extends makeSingleton(AddonModPageIndexLinkHandlerService) {}

View File

@ -0,0 +1,44 @@
// (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.
import { Injectable } from '@angular/core';
import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classes/module-list-handler';
import { makeSingleton } from '@singletons';
import { AddonModPage } from '../page';
/**
* Handler to treat links to page list page.
*/
@Injectable({ providedIn: 'root' })
export class AddonModPageListLinkHandlerService extends CoreContentLinksModuleListHandler {
name = 'AddonModPageListLinkHandler';
constructor() {
super('AddonModPage', 'page');
}
/**
* Check if the handler is enabled on a site level.
*
* @param siteId The site ID.
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(siteId: string): Promise<boolean> {
return AddonModPage.instance.isPluginEnabled(siteId);
}
}
export class AddonModPageListLinkHandler extends makeSingleton(AddonModPageListLinkHandlerService) {}

View File

@ -0,0 +1,93 @@
// (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.
import { Injectable, Type } from '@angular/core';
import { AddonModPage } from '../page';
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
import { CoreConstants } from '@/core/constants';
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
import { CoreCourseModule } from '@features/course/services/course-helper';
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
import { AddonModPageIndexComponent } from '../../components/index';
import { makeSingleton } from '@singletons';
/**
* Handler to support page modules.
*/
@Injectable({ providedIn: 'root' })
export class AddonModPageModuleHandlerService implements CoreCourseModuleHandler {
static readonly PAGE_NAME = 'mod_page';
name = 'AddonModPage';
modName = 'page';
supportedFeatures = {
[CoreConstants.FEATURE_MOD_ARCHETYPE]: CoreConstants.MOD_ARCHETYPE_RESOURCE,
[CoreConstants.FEATURE_GROUPS]: false,
[CoreConstants.FEATURE_GROUPINGS]: false,
[CoreConstants.FEATURE_MOD_INTRO]: true,
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: false,
[CoreConstants.FEATURE_GRADE_OUTCOMES]: false,
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
};
/**
* Check if the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
isEnabled(): Promise<boolean> {
return AddonModPage.instance.isPluginEnabled();
}
/**
* Get the data required to display the module in the course contents view.
*
* @param module The module object.
* @return Data to render the module.
*/
getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
return {
icon: CoreCourse.instance.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
title: module.name,
class: 'addon-mod_page-handler',
showDownloadButton: true,
action(event: Event, module: CoreCourseModule, courseId: number, options?: CoreNavigationOptions): void {
options = options || {};
options.params = options.params || {};
Object.assign(options.params, { module });
const routeParams = '/' + courseId + '/' + module.id;
CoreNavigator.instance.navigateToSitePath(AddonModPageModuleHandlerService.PAGE_NAME + routeParams, options);
},
};
}
/**
* Get the component to render the module. This is needed to support singleactivity course format.
* The component returned must implement CoreCourseModuleMainComponent.
*
* @param course The course object.
* @param module The module object.
* @return The component to use, undefined if not found.
*/
async getMainComponent(): Promise<Type<unknown> | undefined> {
return AddonModPageIndexComponent;
}
}
export class AddonModPageModuleHandler extends makeSingleton(AddonModPageModuleHandlerService) {}

View File

@ -0,0 +1,63 @@
// (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.
import { Injectable } from '@angular/core';
import { CorePluginFileHandler } from '@services/plugin-file-delegate';
import { makeSingleton } from '@singletons';
/**
* Handler to treat links to page.
*/
@Injectable({ providedIn: 'root' })
export class AddonModPagePluginFileHandlerService implements CorePluginFileHandler {
name = 'AddonModPagePluginFileHandler';
component = 'mod_page';
/**
* Return the RegExp to match the revision on pluginfile URLs.
*
* @param args Arguments of the pluginfile URL defining component and filearea at least.
* @return RegExp to match the revision on pluginfile URLs.
*/
getComponentRevisionRegExp(args: string[]): RegExp | undefined {
// Check filearea.
if (args[2] == 'content') {
// Component + Filearea + Revision
return new RegExp('/mod_page/content/([0-9]+)/');
}
}
/**
* Should return the string to remove the revision on pluginfile url.
*
* @return String to remove the revision on pluginfile url.
*/
getComponentRevisionReplace(): string {
// Component + Filearea + Revision
return '/mod_page/content/0/';
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return Whether or not the handler is enabled on a site level.
*/
async isEnabled(): Promise<boolean> {
return true;
}
}
export class AddonModPagePluginFileHandler extends makeSingleton(AddonModPagePluginFileHandlerService) {}

View File

@ -0,0 +1,90 @@
// (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.
import { Injectable } from '@angular/core';
import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler';
import { CoreCourse, CoreCourseAnyModuleData, CoreCourseWSModule } from '@features/course/services/course';
import { CoreUtils } from '@services/utils/utils';
import { makeSingleton } from '@singletons';
import { AddonModPage, AddonModPageProvider } from '../page';
/**
* Handler to prefetch pages.
*/
@Injectable({ providedIn: 'root' })
export class AddonModPagePrefetchHandlerService extends CoreCourseResourcePrefetchHandlerBase {
name = 'AddonModPage';
modName = 'page';
component = AddonModPageProvider.COMPONENT;
updatesNames = /^configuration$|^.*files$/;
/**
* Download or prefetch the content.
*
* @param module The module object returned by WS.
* @param courseId Course ID.
* @param prefetch True to prefetch, false to download right away.
* @return Promise resolved when all content is downloaded. Data returned is not reliable.
*/
async downloadOrPrefetch(module: CoreCourseWSModule, courseId: number, prefetch?: boolean): Promise<void> {
const promises: Promise<unknown>[] = [];
promises.push(super.downloadOrPrefetch(module, courseId, prefetch));
if (AddonModPage.instance.isGetPageWSAvailable()) {
promises.push(AddonModPage.instance.getPageData(courseId, module.id));
}
await Promise.all(promises);
}
/**
* Invalidate the prefetched content.
*
* @param moduleId The module ID.
* @param courseId Course ID the module belongs to.
* @return Promise resolved when the data is invalidated.
*/
async invalidateContent(moduleId: number, courseId: number): Promise<void> {
await AddonModPage.instance.invalidateContent(moduleId, courseId);
}
/**
* Invalidate WS calls needed to determine module status.
*
* @param module Module.
* @param courseId Course ID the module belongs to.
* @return Promise resolved when invalidated.
*/
async invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
const promises: Promise<unknown>[] = [];
promises.push(AddonModPage.instance.invalidatePageData(courseId));
promises.push(CoreCourse.instance.invalidateModule(module.id));
await CoreUtils.instance.allPromises(promises);
}
/**
* Whether or not the handler is enabled on a site level.
*
* @return A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
*/
isEnabled(): Promise<boolean> {
return AddonModPage.instance.isPluginEnabled();
}
}
export class AddonModPagePrefetchHandler extends makeSingleton(AddonModPagePrefetchHandlerService) {}

View File

@ -0,0 +1,105 @@
// (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.
import { Injectable } from '@angular/core';
import { AddonModPageProvider } from './page';
import { CoreError } from '@classes/errors/error';
import { CoreTextUtils } from '@services/utils/text';
import { CoreFile } from '@services/file';
import { CoreSites } from '@services/sites';
import { CoreFilepool } from '@services/filepool';
import { CoreWS } from '@services/ws';
import { CoreDomUtils } from '@services/utils/dom';
import { makeSingleton } from '@singletons';
import { CoreCourseModuleContentFile } from '@features/course/services/course';
/**
* Service that provides some features for page.
*/
@Injectable({ providedIn: 'root' })
export class AddonModPageHelperProvider {
/**
* Gets the page HTML.
*
* @param contents The module contents.
* @param moduleId The module ID.
* @return The HTML of the page.
*/
async getPageHtml(contents: CoreCourseModuleContentFile[], moduleId: number): Promise<string> {
let indexUrl: string | undefined;
const paths: Record<string, string> = {};
// Extract the information about paths from the module contents.
contents.forEach((content) => {
const url = content.fileurl;
if (this.isMainPage(content)) {
// This seems to be the most reliable way to spot the index page.
indexUrl = url;
} else {
let key = content.filename;
if (content.filepath !== '/') {
// Add the folders without the leading slash.
key = content.filepath.substr(1) + key;
}
paths[CoreTextUtils.instance.decodeURIComponent(key)] = url;
}
});
// Promise handling when we are in a browser.
if (!indexUrl) {
// If ever that happens.
throw new CoreError('Could not locate the index page');
}
let url: string;
if (CoreFile.instance.isAvailable()) {
// The file system is available.
url = await CoreFilepool.instance.downloadUrl(
CoreSites.instance.getCurrentSiteId(),
indexUrl,
false,
AddonModPageProvider.COMPONENT,
moduleId,
);
} else {
// We return the live URL.
url = await CoreSites.instance.getCurrentSite()?.checkAndFixPluginfileURL(indexUrl) || '';
}
const content = await CoreWS.instance.getText(url);
// Now that we have the content, we update the SRC to point back to the external resource.
// That will be caught by core-format-text.
return CoreDomUtils.instance.restoreSourcesInHtml(content, paths);
}
/**
* Returns whether the file is the main page of the module.
*
* @param file An object returned from WS containing file info.
* @return Whether the file is the main page or not.
*/
protected isMainPage(file: CoreCourseModuleContentFile): boolean {
const filename = file.filename || '';
const fileurl = file.fileurl || '';
const url = '/mod_page/content/index.html';
const encodedUrl = encodeURIComponent(url);
return (filename === 'index.html' && (fileurl.indexOf(url) > 0 || fileurl.indexOf(encodedUrl) > 0 ));
}
}
export class AddonModPageHelper extends makeSingleton(AddonModPageHelperProvider) {}

View File

@ -0,0 +1,225 @@
// (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.
import { Injectable } from '@angular/core';
import { CoreSitesCommonWSOptions, CoreSites } from '@services/sites';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreWSExternalWarning, CoreWSExternalFile } from '@services/ws';
import { makeSingleton } from '@singletons';
import { CoreFilepool } from '@services/filepool';
import { CoreCourse } from '@features/course/services/course';
import { CoreUtils } from '@services/utils/utils';
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
import { CoreWSError } from '@classes/errors/wserror';
const ROOT_CACHE_KEY = 'mmaModPage:';
/**
* Service that provides some features for page.
*/
@Injectable({ providedIn: 'root' })
export class AddonModPageProvider {
static readonly COMPONENT = 'mmaModPage';
/**
* Get a page by course module ID.
*
* @param courseId Course ID.
* @param cmId Course module ID.
* @param options Other options.
* @return Promise resolved when the page is retrieved.
*/
getPageData(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModPagePage> {
return this.getPageByKey(courseId, 'coursemodule', cmId, options);
}
/**
* Get a page.
*
* @param courseId Course ID.
* @param key Name of the property to check.
* @param value Value to search.
* @param options Other options.
* @return Promise resolved when the page is retrieved.
*/
protected async getPageByKey(
courseId: number,
key: string,
value: number,
options: CoreSitesCommonWSOptions = {},
): Promise<AddonModPagePage> {
const site = await CoreSites.instance.getSite(options.siteId);
const params: AddonModPageGetPagesByCoursesWSParams = {
courseids: [courseId],
};
const preSets: CoreSiteWSPreSets = {
cacheKey: this.getPageCacheKey(courseId),
updateFrequency: CoreSite.FREQUENCY_RARELY,
component: AddonModPageProvider.COMPONENT,
...CoreSites.instance.getReadingStrategyPreSets(options.readingStrategy),
};
const response = await site.read<AddonModPageGetPagesByCoursesWSResponse>('mod_page_get_pages_by_courses', params, preSets);
const currentPage = response.pages.find((page) => page[key] == value);
if (currentPage) {
return currentPage;
}
throw new CoreWSError('Page not found');
}
/**
* Get cache key for page data WS calls.
*
* @param courseId Course ID.
* @return Cache key.
*/
protected getPageCacheKey(courseId: number): string {
return ROOT_CACHE_KEY + 'page:' + courseId;
}
/**
* Invalidate the prefetched content.
*
* @param moduleId The module ID.
* @param courseId Course ID of the module.
* @param siteId Site ID. If not defined, current site.
*/
invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId();
const promises: Promise<void>[] = [];
promises.push(this.invalidatePageData(courseId, siteId));
promises.push(CoreFilepool.instance.invalidateFilesByComponent(siteId, AddonModPageProvider.COMPONENT, moduleId));
promises.push(CoreCourse.instance.invalidateModule(moduleId, siteId));
return CoreUtils.instance.allPromises(promises);
}
/**
* Invalidates page data.
*
* @param courseId Course ID.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the data is invalidated.
*/
async invalidatePageData(courseId: number, siteId?: string): Promise<void> {
const site = await CoreSites.instance.getSite(siteId);
await site.invalidateWsCacheForKey(this.getPageCacheKey(courseId));
}
/**
* Returns whether or not getPage WS available or not.
*
* @return If WS is avalaible.
* @since 3.3
*/
isGetPageWSAvailable(): boolean {
return CoreSites.instance.wsAvailableInCurrentSite('mod_page_get_pages_by_courses');
}
/**
* Return whether or not the plugin is enabled.
*
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with true if plugin is enabled, rejected or resolved with false otherwise.
*/
async isPluginEnabled(siteId?: string): Promise<boolean> {
const site = await CoreSites.instance.getSite(siteId);
return site.canDownloadFiles();
}
/**
* Report a page as being viewed.
*
* @param pageid Module ID.
* @param name Name of the page.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved when the WS call is successful.
*/
logView(pageid: number, name?: string, siteId?: string): Promise<void> {
const params: AddonModPageViewPageWSParams = {
pageid,
};
return CoreCourseLogHelper.instance.logSingle(
'mod_page_view_page',
params,
AddonModPageProvider.COMPONENT,
pageid,
name,
'page',
{},
siteId,
);
}
}
export class AddonModPage extends makeSingleton(AddonModPageProvider) {}
/**
* Page returned by mod_page_get_pages_by_courses.
*/
export type AddonModPagePage = {
id: number; // Module id.
coursemodule: number; // Course module id.
course: number; // Course id.
name: string; // Page name.
intro: string; // Summary.
introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
introfiles: CoreWSExternalFile[];
content: string; // Page content.
contentformat: number; // Content format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
contentfiles: CoreWSExternalFile[];
legacyfiles: number; // Legacy files flag.
legacyfileslast: number; // Legacy files last control flag.
display: number; // How to display the page.
displayoptions: string; // Display options (width, height).
revision: number; // Incremented when after each file changes, to avoid cache.
timemodified: number; // Last time the page was modified.
section: number; // Course section id.
visible: number; // Module visibility.
groupmode: number; // Group mode.
groupingid: number; // Grouping id.
};
/**
* Result of WS mod_page_get_pages_by_courses.
*/
type AddonModPageGetPagesByCoursesWSResponse = {
pages: AddonModPagePage[];
warnings?: CoreWSExternalWarning[];
};
/**
* Params of mod_page_view_page WS.
*/
type AddonModPageViewPageWSParams = {
pageid: number; // Page instance id.
};
/**
* Params of mod_page_get_pages_by_courses WS.
*/
type AddonModPageGetPagesByCoursesWSParams = {
courseids?: number[]; // Array of course ids.
};

View File

@ -310,7 +310,7 @@ export interface CorePluginFileHandler extends CoreDelegateHandler {
* @param args Arguments of the pluginfile URL defining component and filearea at least. * @param args Arguments of the pluginfile URL defining component and filearea at least.
* @return RegExp to match the revision on pluginfile URLs. * @return RegExp to match the revision on pluginfile URLs.
*/ */
getComponentRevisionRegExp?(args: string[]): RegExp; getComponentRevisionRegExp?(args: string[]): RegExp | undefined;
/** /**
* Should return the string to remove the revision on pluginfile url. * Should return the string to remove the revision on pluginfile url.