commit
832dcf8bf2
|
@ -3749,6 +3749,14 @@
|
||||||
"@babel/types": "^7.3.0"
|
"@babel/types": "^7.3.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/chart.js": {
|
||||||
|
"version": "2.9.31",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/chart.js/-/chart.js-2.9.31.tgz",
|
||||||
|
"integrity": "sha512-hzS6phN/kx3jClk3iYqEHNnYIRSi4RZrIGJ8CDLjgatpHoftCezvC44uqB3o3OUm9ftU1m7sHG8+RLyPTlACrA==",
|
||||||
|
"requires": {
|
||||||
|
"moment": "^2.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"@types/cordova": {
|
"@types/cordova": {
|
||||||
"version": "0.0.34",
|
"version": "0.0.34",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
|
||||||
|
@ -5930,6 +5938,47 @@
|
||||||
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
|
||||||
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
"integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA=="
|
||||||
},
|
},
|
||||||
|
"chart.js": {
|
||||||
|
"version": "2.9.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.4.tgz",
|
||||||
|
"integrity": "sha512-B07aAzxcrikjAPyV+01j7BmOpxtQETxTSlQ26BEYJ+3iUkbNKaOJ/nDbT6JjyqYxseM0ON12COHYdU2cTIjC7A==",
|
||||||
|
"requires": {
|
||||||
|
"chartjs-color": "^2.1.0",
|
||||||
|
"moment": "^2.10.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chartjs-color": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-color/-/chartjs-color-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-haqOg1+Yebys/Ts/9bLo/BqUcONQOdr/hoEr2LLTRl6C5LXctUdHxsCYfvQVg5JIxITrfCNUDr4ntqmQk9+/0w==",
|
||||||
|
"requires": {
|
||||||
|
"chartjs-color-string": "^0.6.0",
|
||||||
|
"color-convert": "^1.9.3"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"color-convert": {
|
||||||
|
"version": "1.9.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
|
||||||
|
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
|
||||||
|
"requires": {
|
||||||
|
"color-name": "1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-name": {
|
||||||
|
"version": "1.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
|
||||||
|
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chartjs-color-string": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/chartjs-color-string/-/chartjs-color-string-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-TIB5OKn1hPJvO7JcteW4WY/63v6KwEdt6udfnDE9iCAZgy+V4SrbSxoIbTw/xkUIapjEI4ExGtD0+6D3KyFd7A==",
|
||||||
|
"requires": {
|
||||||
|
"color-name": "^1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"check-es-compat": {
|
"check-es-compat": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/check-es-compat/-/check-es-compat-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/check-es-compat/-/check-es-compat-1.1.1.tgz",
|
||||||
|
|
|
@ -68,9 +68,11 @@
|
||||||
"@ionic/angular": "^5.6.3",
|
"@ionic/angular": "^5.6.3",
|
||||||
"@ngx-translate/core": "^13.0.0",
|
"@ngx-translate/core": "^13.0.0",
|
||||||
"@ngx-translate/http-loader": "^6.0.0",
|
"@ngx-translate/http-loader": "^6.0.0",
|
||||||
|
"@types/chart.js": "^2.9.31",
|
||||||
"@types/cordova": "0.0.34",
|
"@types/cordova": "0.0.34",
|
||||||
"@types/cordova-plugin-file-transfer": "^1.6.2",
|
"@types/cordova-plugin-file-transfer": "^1.6.2",
|
||||||
"@types/dom-mediacapture-record": "^1.0.7",
|
"@types/dom-mediacapture-record": "^1.0.7",
|
||||||
|
"chart.js": "^2.9.4",
|
||||||
"com-darryncampbell-cordova-plugin-intent": "^1.3.0",
|
"com-darryncampbell-cordova-plugin-intent": "^1.3.0",
|
||||||
"cordova": "^10.0.0",
|
"cordova": "^10.0.0",
|
||||||
"cordova-android": "^8.1.0",
|
"cordova-android": "^8.1.0",
|
||||||
|
|
|
@ -14,19 +14,25 @@
|
||||||
|
|
||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule, Routes } from '@angular/router';
|
import { RouterModule, Routes } from '@angular/router';
|
||||||
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { AddonModChoiceComponentsModule } from './components/components.module';
|
||||||
|
import { AddonModChoiceIndexPage } from './pages/index/index';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'module/:courseId/:cmId',
|
path: ':courseId/:cmId',
|
||||||
loadChildren: () => import('./pages/module-index/module-index.module').then( m => m.CoreSitePluginsModuleIndexPageModule),
|
component: AddonModChoiceIndexPage,
|
||||||
},
|
|
||||||
{
|
|
||||||
path: ':component/:method/:hash',
|
|
||||||
loadChildren: () => import('./pages/plugin-page/plugin-page.module').then( m => m.CoreSitePluginsPluginPageModule),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [RouterModule.forChild(routes)],
|
imports: [
|
||||||
|
RouterModule.forChild(routes),
|
||||||
|
CoreSharedModule,
|
||||||
|
AddonModChoiceComponentsModule,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
AddonModChoiceIndexPage,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class CoreSitePluginsLazyModule {}
|
export class AddonModChoiceLazyModule {}
|
|
@ -0,0 +1,73 @@
|
||||||
|
// (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, Type } 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 { CoreCronDelegate } from '@services/cron';
|
||||||
|
import { CORE_SITE_SCHEMAS } from '@services/sites';
|
||||||
|
import { AddonModChoiceComponentsModule } from './components/components.module';
|
||||||
|
import { AddonModChoiceProvider } from './services/choice';
|
||||||
|
import { AddonModChoiceOfflineProvider } from './services/choice-offline';
|
||||||
|
import { AddonModChoiceSyncProvider } from './services/choice-sync';
|
||||||
|
import { OFFLINE_SITE_SCHEMA } from './services/database/choice';
|
||||||
|
import { AddonModChoiceIndexLinkHandler } from './services/handlers/index-link';
|
||||||
|
import { AddonModChoiceListLinkHandler } from './services/handlers/list-link';
|
||||||
|
import { AddonModChoiceModuleHandler, AddonModChoiceModuleHandlerService } from './services/handlers/module';
|
||||||
|
import { AddonModChoicePrefetchHandler } from './services/handlers/prefetch';
|
||||||
|
import { AddonModChoiceSyncCronHandler } from './services/handlers/sync-cron';
|
||||||
|
|
||||||
|
export const ADDON_MOD_CHOICE_SERVICES: Type<unknown>[] = [
|
||||||
|
AddonModChoiceProvider,
|
||||||
|
AddonModChoiceOfflineProvider,
|
||||||
|
AddonModChoiceSyncProvider,
|
||||||
|
];
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
{
|
||||||
|
path: AddonModChoiceModuleHandlerService.PAGE_NAME,
|
||||||
|
loadChildren: () => import('./choice-lazy.module').then(m => m.AddonModChoiceLazyModule),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CoreMainMenuTabRoutingModule.forChild(routes),
|
||||||
|
AddonModChoiceComponentsModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: CORE_SITE_SCHEMAS,
|
||||||
|
useValue: [OFFLINE_SITE_SCHEMA],
|
||||||
|
multi: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: APP_INITIALIZER,
|
||||||
|
multi: true,
|
||||||
|
deps: [],
|
||||||
|
useFactory: () => () => {
|
||||||
|
CoreCourseModuleDelegate.registerHandler(AddonModChoiceModuleHandler.instance);
|
||||||
|
CoreCourseModulePrefetchDelegate.registerHandler(AddonModChoicePrefetchHandler.instance);
|
||||||
|
CoreCronDelegate.register(AddonModChoiceSyncCronHandler.instance);
|
||||||
|
CoreContentLinksDelegate.registerHandler(AddonModChoiceIndexLinkHandler.instance);
|
||||||
|
CoreContentLinksDelegate.registerHandler(AddonModChoiceListLinkHandler.instance);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModChoiceModule {}
|
|
@ -0,0 +1,34 @@
|
||||||
|
// (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 { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { CoreCourseComponentsModule } from '@features/course/components/components.module';
|
||||||
|
import { AddonModChoiceIndexComponent } from './index/index';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
declarations: [
|
||||||
|
AddonModChoiceIndexComponent,
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
CoreSharedModule,
|
||||||
|
CoreCourseComponentsModule,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
AddonModChoiceIndexComponent,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AddonModChoiceComponentsModule {}
|
|
@ -0,0 +1,175 @@
|
||||||
|
<!-- 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 *ngIf="loaded && !hasOffline && isOnline" [priority]="700" [content]="'core.refresh' | translate"
|
||||||
|
(action)="doRefresh(null, $event)" [iconAction]="refreshIcon" [closeOnClick]="false">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="loaded && hasOffline && isOnline" [priority]="600" (action)="doRefresh(null, $event, true)"
|
||||||
|
[content]="'core.settings.synchronizenow' | translate" [iconAction]="syncIcon" [closeOnClick]="false">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch($event)"
|
||||||
|
[iconAction]="prefetchStatusIcon" [closeOnClick]="false">
|
||||||
|
</core-context-menu-item>
|
||||||
|
<core-context-menu-item *ngIf="size" [priority]="400" [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">
|
||||||
|
|
||||||
|
<core-course-module-description [description]="description" [component]="component" [componentId]="componentId"
|
||||||
|
contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||||
|
</core-course-module-description>
|
||||||
|
|
||||||
|
<!-- Activity availability messages -->
|
||||||
|
<ion-card class="core-info-card" *ngIf="choiceNotOpenYet">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
|
||||||
|
<ion-label>
|
||||||
|
<p *ngIf="options.length">{{ 'addon.mod_choice.previewonly' | translate:{$a: openTimeReadable} }}</p>
|
||||||
|
<p *ngIf="!options.length">{{ 'addon.mod_choice.notopenyet' | translate:{$a: openTimeReadable} }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<ion-card class="core-info-card" *ngIf="choiceClosed">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
|
||||||
|
<ion-label>
|
||||||
|
<p *ngIf="options.length">
|
||||||
|
{{ 'addon.mod_choice.yourselection' | translate }}
|
||||||
|
<core-format-text [text]="options[0].text" contextLevel="module" [contextInstanceId]="module.id"
|
||||||
|
[courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</p>
|
||||||
|
<p>{{ 'addon.mod_choice.expired' | translate:{$a: closeTimeReadable} }}</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- Choice done in offline but not synchronized -->
|
||||||
|
<ion-card class="core-warning-card" *ngIf="hasOffline">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="fas-exclamation-triangle" slot="start"></ion-icon>
|
||||||
|
<ion-label>{{ 'core.hasdatatosync' | translate:{$a: moduleName} }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- Inform what will happen with the choices. -->
|
||||||
|
<ion-card class="core-info-card" *ngIf="canEdit && publishInfo && options.length">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
|
||||||
|
<ion-label>{{ publishInfo | translate }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- Choice options -->
|
||||||
|
<ion-card *ngIf="options.length && choice">
|
||||||
|
<ng-container *ngIf="choice.allowmultiple">
|
||||||
|
<ion-item class="ion-text-wrap" *ngFor="let option of options">
|
||||||
|
<ion-label>
|
||||||
|
<ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}"></ng-container>
|
||||||
|
</ion-label>
|
||||||
|
<ion-checkbox slot="end" [(ngModel)]="option.checked" [disabled]="option.disabled || !canEdit"></ion-checkbox>
|
||||||
|
</ion-item>
|
||||||
|
</ng-container>
|
||||||
|
<ion-radio-group *ngIf="!choice.allowmultiple" [(ngModel)]="selectedOption.id">
|
||||||
|
<ion-item class="ion-text-wrap" *ngFor="let option of options">
|
||||||
|
<ion-label>
|
||||||
|
<ng-container *ngTemplateOutlet="optionLabelTemplate; context: {option: option}"></ng-container>
|
||||||
|
</ion-label>
|
||||||
|
<ion-radio slot="end" [value]="option.id" [disabled]="option.disabled || !canEdit"></ion-radio>
|
||||||
|
</ion-item>
|
||||||
|
</ion-radio-group>
|
||||||
|
<ion-button *ngIf="canEdit" expand="block" (click)="save()" [disabled]="!canSave()" class="ion-margin">
|
||||||
|
{{ 'addon.mod_choice.savemychoice' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
<ion-button *ngIf="canDelete" expand="block" color="light" (click)="delete()" class="ion-margin">
|
||||||
|
{{ 'addon.mod_choice.removemychoice' | translate }}
|
||||||
|
</ion-button>
|
||||||
|
</ion-card>
|
||||||
|
|
||||||
|
<!-- Choice results -->
|
||||||
|
<div *ngIf="canSeeResults && choice">
|
||||||
|
<ion-item-divider>
|
||||||
|
<ion-label>
|
||||||
|
<h2>{{ 'addon.mod_choice.responses' | translate }}</h2>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item-divider>
|
||||||
|
<ion-grid class="ion-no-padding">
|
||||||
|
<ion-row>
|
||||||
|
<ion-col size="12" size-lg="5">
|
||||||
|
<ion-item class="ion-text-wrap core-warning-item" *ngIf="hasOffline">
|
||||||
|
<ion-icon slot="start" name="fas-exclamation-triangle" color="warning"></ion-icon>
|
||||||
|
<ion-label>{{ 'addon.mod_choice.resultsnotsynced' | translate }}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
<ion-item>
|
||||||
|
<ion-label>
|
||||||
|
<core-chart type="pie" [data]="data" [labels]="labels" height="300" contextLevel="module"
|
||||||
|
[contextInstanceId]="module.id" [courseId]="courseId">
|
||||||
|
</core-chart>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-col>
|
||||||
|
<ion-col *ngIf="choice.publish && results" size="12" size-lg="7">
|
||||||
|
<ion-item-group *ngFor="let result of results">
|
||||||
|
<ion-item-divider class="ion-text-wrap">
|
||||||
|
<ion-label>
|
||||||
|
<h2>
|
||||||
|
<core-format-text [text]="result.text" contextLevel="module" [contextInstanceId]="module.id"
|
||||||
|
[courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
</h2>
|
||||||
|
<p>
|
||||||
|
{{ 'addon.mod_choice.numberofuser' | translate }}: {{ result.numberofuser }}
|
||||||
|
({{ 'core.percentagenumber' | translate: {$a: result.percentageamountfixed} }})
|
||||||
|
</p>
|
||||||
|
<p *ngIf="choice.limitanswers && choice.showavailable">
|
||||||
|
{{ 'addon.mod_choice.limita' | translate:{$a: result.maxanswer} }}
|
||||||
|
</p>
|
||||||
|
</ion-label>
|
||||||
|
</ion-item-divider>
|
||||||
|
<ion-item *ngFor="let user of result.userresponses" core-user-link [courseId]="courseId"
|
||||||
|
[userId]="user.userid" [title]="user.fullname" class="ion-text-wrap">
|
||||||
|
<core-user-avatar [user]="user" slot="start" [courseId]="courseId"></core-user-avatar>
|
||||||
|
<ion-label><p>{{user.fullname}}</p></ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-item-group>
|
||||||
|
</ion-col>
|
||||||
|
</ion-row>
|
||||||
|
</ion-grid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ion-card class="core-info-card" *ngIf="!canSeeResults && !choiceNotOpenYet">
|
||||||
|
<ion-item>
|
||||||
|
<ion-icon name="fas-info-circle" slot="start"></ion-icon>
|
||||||
|
<ion-label><p>{{ 'addon.mod_choice.noresultsviewable' | translate }}</p></ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-card>
|
||||||
|
</core-loading>
|
||||||
|
|
||||||
|
<!-- Template to render a choice option label. -->
|
||||||
|
<ng-template #optionLabelTemplate let-option="option">
|
||||||
|
<p>
|
||||||
|
<core-format-text [text]="option.text" contextLevel="module" [contextInstanceId]="module.id" [courseId]="courseId">
|
||||||
|
</core-format-text>
|
||||||
|
<span *ngIf="choice!.limitanswers && option.countanswers >= option.maxanswers">
|
||||||
|
{{ 'addon.mod_choice.full' | translate }}
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
<ng-container *ngIf="choice!.limitanswers && choice!.showavailable">
|
||||||
|
<p>{{ 'addon.mod_choice.responsesa' | translate:{$a: option.countanswers} }}</p>
|
||||||
|
<p>{{ 'addon.mod_choice.limita' | translate:{$a: option.maxanswers} }}</p>
|
||||||
|
</ng-container>
|
||||||
|
</ng-template>
|
|
@ -0,0 +1,479 @@
|
||||||
|
// (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, Optional, OnInit } from '@angular/core';
|
||||||
|
import { CoreCourseModuleMainActivityComponent } from '@features/course/classes/main-activity-component';
|
||||||
|
import { CoreCourseContentsPage } from '@features/course/pages/contents/contents';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
import { IonContent } from '@ionic/angular';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreDomUtils } from '@services/utils/dom';
|
||||||
|
import { CoreTimeUtils } from '@services/utils/time';
|
||||||
|
import { Translate } from '@singletons';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
|
import {
|
||||||
|
AddonModChoice,
|
||||||
|
AddonModChoiceChoice,
|
||||||
|
AddonModChoiceOption,
|
||||||
|
AddonModChoiceProvider,
|
||||||
|
AddonModChoiceResult,
|
||||||
|
} from '../../services/choice';
|
||||||
|
import { AddonModChoiceOffline } from '../../services/choice-offline';
|
||||||
|
import {
|
||||||
|
AddonModChoiceAutoSyncData,
|
||||||
|
AddonModChoiceSync,
|
||||||
|
AddonModChoiceSyncProvider,
|
||||||
|
AddonModChoiceSyncResult,
|
||||||
|
} from '../../services/choice-sync';
|
||||||
|
import { AddonModChoicePrefetchHandler } from '../../services/handlers/prefetch';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component that displays a choice.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'addon-mod-choice-index',
|
||||||
|
templateUrl: 'addon-mod-choice-index.html',
|
||||||
|
})
|
||||||
|
export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityComponent implements OnInit {
|
||||||
|
|
||||||
|
component = AddonModChoiceProvider.COMPONENT;
|
||||||
|
moduleName = 'choice';
|
||||||
|
|
||||||
|
choice?: AddonModChoiceChoice;
|
||||||
|
options: AddonModChoiceOption[] = [];
|
||||||
|
selectedOption: {id: number} = { id: -1 };
|
||||||
|
choiceNotOpenYet = false;
|
||||||
|
choiceClosed = false;
|
||||||
|
canEdit = false;
|
||||||
|
canDelete = false;
|
||||||
|
canSeeResults = false;
|
||||||
|
data: number[] = [];
|
||||||
|
labels: string[] = [];
|
||||||
|
results: AddonModChoiceResultFormatted[] = [];
|
||||||
|
publishInfo?: string; // Message explaining the user what will happen with his choices.
|
||||||
|
openTimeReadable?: string;
|
||||||
|
closeTimeReadable?: string;
|
||||||
|
|
||||||
|
protected userId?: number;
|
||||||
|
protected syncEventName = AddonModChoiceSyncProvider.AUTO_SYNCED;
|
||||||
|
protected hasAnsweredOnline = false;
|
||||||
|
protected now = Date.now();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected content?: IonContent,
|
||||||
|
@Optional() courseContentsPage?: CoreCourseContentsPage,
|
||||||
|
) {
|
||||||
|
super('AddonModChoiceIndexComponent', content, courseContentsPage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
super.ngOnInit();
|
||||||
|
|
||||||
|
this.userId = CoreSites.getCurrentSiteUserId();
|
||||||
|
|
||||||
|
await this.loadContent(false, true);
|
||||||
|
|
||||||
|
if (!this.choice) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AddonModChoice.logView(this.choice.id, this.choice.name);
|
||||||
|
|
||||||
|
await CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||||
|
} catch {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async invalidateContent(): Promise<void> {
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
promises.push(AddonModChoice.invalidateChoiceData(this.courseId));
|
||||||
|
|
||||||
|
if (this.choice) {
|
||||||
|
promises.push(AddonModChoice.invalidateOptions(this.choice.id));
|
||||||
|
promises.push(AddonModChoice.invalidateResults(this.choice.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected isRefreshSyncNeeded(syncEventData: AddonModChoiceAutoSyncData): boolean {
|
||||||
|
if (this.choice && syncEventData.choiceId == this.choice.id && syncEventData.userId == this.userId) {
|
||||||
|
this.content?.scrollToTop();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
protected async fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<void> {
|
||||||
|
this.now = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.choice = await AddonModChoice.getChoice(this.courseId, this.module.id);
|
||||||
|
|
||||||
|
if (sync) {
|
||||||
|
// Try to synchronize the choice.
|
||||||
|
const updated = await this.syncActivity(showErrors);
|
||||||
|
|
||||||
|
if (updated) {
|
||||||
|
// Responses were sent, update the choice.
|
||||||
|
this.choice = await AddonModChoice.getChoice(this.courseId, this.module.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.choice.timeopen = (this.choice.timeopen || 0) * 1000;
|
||||||
|
this.choice.timeclose = (this.choice.timeclose || 0) * 1000;
|
||||||
|
this.openTimeReadable = CoreTimeUtils.userDate(this.choice.timeopen);
|
||||||
|
this.closeTimeReadable = CoreTimeUtils.userDate(this.choice.timeclose);
|
||||||
|
|
||||||
|
this.description = this.choice.intro;
|
||||||
|
this.choiceNotOpenYet = !!this.choice.timeopen && this.choice.timeopen > this.now;
|
||||||
|
this.choiceClosed = !!this.choice.timeclose && this.choice.timeclose <= this.now;
|
||||||
|
|
||||||
|
this.dataRetrieved.emit(this.choice);
|
||||||
|
|
||||||
|
// Check if there are responses stored in offline.
|
||||||
|
this.hasOffline = await AddonModChoiceOffline.hasResponse(this.choice.id);
|
||||||
|
|
||||||
|
// We need fetchOptions to finish before calling fetchResults because it needs hasAnsweredOnline variable.
|
||||||
|
await this.fetchOptions(this.choice);
|
||||||
|
|
||||||
|
await this.fetchResults(this.choice);
|
||||||
|
} finally {
|
||||||
|
this.fillContextMenu(refresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to get choice options.
|
||||||
|
*
|
||||||
|
* @param choice Choice data.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchOptions(choice: AddonModChoiceChoice): Promise<void> {
|
||||||
|
let options = await AddonModChoice.getOptions(choice.id, { cmId: this.module.id });
|
||||||
|
|
||||||
|
// Check if the user has answered (synced) to allow show results.
|
||||||
|
this.hasAnsweredOnline = options.some((option) => option.checked);
|
||||||
|
|
||||||
|
if (this.hasOffline) {
|
||||||
|
options = await this.getOfflineResponses(choice, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isOpen = this.isChoiceOpen(choice);
|
||||||
|
|
||||||
|
this.selectedOption = { id: -1 }; // Single choice model.
|
||||||
|
const hasAnswered = options.some((option) => {
|
||||||
|
if (!option.checked) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!choice.allowmultiple) {
|
||||||
|
this.selectedOption.id = option.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.canEdit = isOpen && (choice.allowupdate! || !hasAnswered);
|
||||||
|
this.canDelete = isOpen && choice.allowupdate! && hasAnswered;
|
||||||
|
this.options = options;
|
||||||
|
|
||||||
|
if (!this.canEdit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate the publish info message.
|
||||||
|
switch (choice.showresults) {
|
||||||
|
case AddonModChoiceProvider.RESULTS_NOT:
|
||||||
|
this.publishInfo = 'addon.mod_choice.publishinfonever';
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AddonModChoiceProvider.RESULTS_AFTER_ANSWER:
|
||||||
|
if (choice.publish == AddonModChoiceProvider.PUBLISH_ANONYMOUS) {
|
||||||
|
this.publishInfo = 'addon.mod_choice.publishinfoanonafter';
|
||||||
|
} else {
|
||||||
|
this.publishInfo = 'addon.mod_choice.publishinfofullafter';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case AddonModChoiceProvider.RESULTS_AFTER_CLOSE:
|
||||||
|
if (choice.publish == AddonModChoiceProvider.PUBLISH_ANONYMOUS) {
|
||||||
|
this.publishInfo = 'addon.mod_choice.publishinfoanonclose';
|
||||||
|
} else {
|
||||||
|
this.publishInfo = 'addon.mod_choice.publishinfofullclose';
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// No need to inform the user since it's obvious that the results are being published.
|
||||||
|
this.publishInfo = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get offline responses.
|
||||||
|
*
|
||||||
|
* @param choice Choice.
|
||||||
|
* @param options Online options.
|
||||||
|
* @return Promise resolved with the options.
|
||||||
|
*/
|
||||||
|
protected async getOfflineResponses(
|
||||||
|
choice: AddonModChoiceChoice,
|
||||||
|
options: AddonModChoiceOption[],
|
||||||
|
): Promise<AddonModChoiceOption[]> {
|
||||||
|
const response = await AddonModChoiceOffline.getResponse(choice.id);
|
||||||
|
|
||||||
|
const optionsMap: {[id: number]: AddonModChoiceOption} = {};
|
||||||
|
options.forEach((option) => {
|
||||||
|
optionsMap[option.id] = option;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update options with the offline data.
|
||||||
|
if (response.deleting) {
|
||||||
|
// Uncheck selected options.
|
||||||
|
if (response.responses.length > 0) {
|
||||||
|
// Uncheck all options selected in responses.
|
||||||
|
response.responses.forEach((selected) => {
|
||||||
|
if (optionsMap[selected] && optionsMap[selected].checked) {
|
||||||
|
optionsMap[selected].checked = false;
|
||||||
|
optionsMap[selected].countanswers--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// On empty responses, uncheck all selected.
|
||||||
|
Object.keys(optionsMap).forEach((key) => {
|
||||||
|
if (optionsMap[key].checked) {
|
||||||
|
optionsMap[key].checked = false;
|
||||||
|
optionsMap[key].countanswers--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Uncheck all options to check again the offlines'.
|
||||||
|
Object.keys(optionsMap).forEach((key) => {
|
||||||
|
if (optionsMap[key].checked) {
|
||||||
|
optionsMap[key].checked = false;
|
||||||
|
optionsMap[key].countanswers--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Then check selected ones.
|
||||||
|
response.responses.forEach((selected) => {
|
||||||
|
if (optionsMap[selected]) {
|
||||||
|
optionsMap[selected].checked = true;
|
||||||
|
optionsMap[selected].countanswers++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert it again to array.
|
||||||
|
return Object.keys(optionsMap).map((key) => optionsMap[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience function to get choice results.
|
||||||
|
*
|
||||||
|
* @param choice Choice.
|
||||||
|
* @return Resolved when done.
|
||||||
|
*/
|
||||||
|
protected async fetchResults(choice: AddonModChoiceChoice): Promise<void> {
|
||||||
|
if (this.choiceNotOpenYet) {
|
||||||
|
// Cannot see results yet.
|
||||||
|
this.canSeeResults = false;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await AddonModChoice.getResults(choice.id, { cmId: this.module.id });
|
||||||
|
|
||||||
|
let hasVotes = false;
|
||||||
|
this.data = [];
|
||||||
|
this.labels = [];
|
||||||
|
|
||||||
|
this.results = results.map((result: AddonModChoiceResultFormatted) => {
|
||||||
|
if (result.numberofuser > 0) {
|
||||||
|
hasVotes = true;
|
||||||
|
}
|
||||||
|
this.data.push(result.numberofuser);
|
||||||
|
this.labels.push(result.text);
|
||||||
|
|
||||||
|
return Object.assign(result, { percentageamountfixed: result.percentageamount.toFixed(1) });
|
||||||
|
});
|
||||||
|
this.canSeeResults = hasVotes || AddonModChoice.canStudentSeeResults(choice, this.hasAnsweredOnline);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a choice is open.
|
||||||
|
*
|
||||||
|
* @param choice Choice data.
|
||||||
|
* @return True if choice is open, false otherwise.
|
||||||
|
*/
|
||||||
|
protected isChoiceOpen(choice: AddonModChoiceChoice): boolean {
|
||||||
|
return (!choice.timeopen || choice.timeopen <= this.now) && (!choice.timeclose || choice.timeclose > this.now);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return true if the user has selected at least one option.
|
||||||
|
*
|
||||||
|
* @return True if the user has responded.
|
||||||
|
*/
|
||||||
|
canSave(): boolean {
|
||||||
|
if (!this.choice) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.choice.allowmultiple) {
|
||||||
|
return this.options.some((option) => option.checked);
|
||||||
|
} else {
|
||||||
|
return this.selectedOption.id !== -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save options selected.
|
||||||
|
*/
|
||||||
|
async save(): Promise<void> {
|
||||||
|
const choice = this.choice!;
|
||||||
|
|
||||||
|
// Only show confirm if choice doesn't allow update.
|
||||||
|
if (!choice.allowupdate) {
|
||||||
|
await CoreDomUtils.showConfirm(Translate.instant('core.areyousure'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const responses: number[] = [];
|
||||||
|
if (choice.allowmultiple) {
|
||||||
|
this.options.forEach((option) => {
|
||||||
|
if (option.checked) {
|
||||||
|
responses.push(option.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
responses.push(this.selectedOption.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const online = await AddonModChoice.submitResponse(choice.id, choice.name, this.courseId, responses);
|
||||||
|
|
||||||
|
this.content?.scrollToTop();
|
||||||
|
|
||||||
|
if (online) {
|
||||||
|
CoreEvents.trigger(CoreEvents.ACTIVITY_DATA_SENT, { module: this.moduleName });
|
||||||
|
// Check completion since it could be configured to complete once the user answers the choice.
|
||||||
|
CoreCourse.checkModuleCompletion(this.courseId, this.module.completiondata);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.dataUpdated(online);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_choice.cannotsubmit', true);
|
||||||
|
} finally {
|
||||||
|
modal.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete options selected.
|
||||||
|
*/
|
||||||
|
async delete(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await CoreDomUtils.showDeleteConfirm();
|
||||||
|
} catch {
|
||||||
|
// User cancelled.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modal = await CoreDomUtils.showModalLoading('core.sending', true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await AddonModChoice.deleteResponses(this.choice!.id, this.choice!.name, this.courseId);
|
||||||
|
|
||||||
|
this.content?.scrollToTop();
|
||||||
|
|
||||||
|
// Refresh the data. Don't call dataUpdated because deleting an answer doesn't mark the choice as outdated.
|
||||||
|
await this.refreshContent(false);
|
||||||
|
} catch (error) {
|
||||||
|
CoreDomUtils.showErrorModalDefault(error, 'addon.mod_choice.cannotsubmit', true);
|
||||||
|
} finally {
|
||||||
|
modal.dismiss();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Function to call when some data has changed. It will refresh/prefetch data.
|
||||||
|
*
|
||||||
|
* @param online Whether the data was sent to server or stored in offline.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async dataUpdated(online: boolean): Promise<void> {
|
||||||
|
if (!online || !this.isPrefetched) {
|
||||||
|
// Not downloaded, just refresh the data.
|
||||||
|
return this.refreshContent(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// The choice is downloaded, update the data.
|
||||||
|
await AddonModChoiceSync.prefetchAfterUpdate(AddonModChoicePrefetchHandler.instance, this.module, this.courseId);
|
||||||
|
|
||||||
|
// Update the view.
|
||||||
|
this.showLoadingAndFetch(false, false);
|
||||||
|
} catch {
|
||||||
|
// Prefetch failed, refresh the data.
|
||||||
|
return this.refreshContent(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs the sync of the activity.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected sync(): Promise<AddonModChoiceSyncResult> {
|
||||||
|
return AddonModChoiceSync.syncChoice(this.choice!.id, this.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if sync has succeed from result sync data.
|
||||||
|
*
|
||||||
|
* @param result Data returned on the sync function.
|
||||||
|
* @return Whether it succeed or not.
|
||||||
|
*/
|
||||||
|
protected hasSyncSucceed(result: AddonModChoiceSyncResult): boolean {
|
||||||
|
return result.updated;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choice result with some calculated data.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceResultFormatted = AddonModChoiceResult & {
|
||||||
|
percentageamountfixed: string; // Percentage of users answers with fixed decimals.
|
||||||
|
};
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"cannotsubmit": "Sorry, there was a problem submitting your choice. Please try again.",
|
||||||
|
"choiceoptions": "Choice options",
|
||||||
|
"errorgetchoice": "Error getting choice data.",
|
||||||
|
"expired": "This activity closed on {{$a}}.",
|
||||||
|
"full": "(Full)",
|
||||||
|
"limita": "Limit: {{$a}}",
|
||||||
|
"modulenameplural": "Choices",
|
||||||
|
"noresultsviewable": "The results are not currently viewable.",
|
||||||
|
"notopenyet": "This activity is not available until {{$a}}.",
|
||||||
|
"numberofuser": "Number of responses",
|
||||||
|
"numberofuserinpercentage": "Percentage of responses",
|
||||||
|
"previewonly": "This is just a preview of the available options for this activity. You will not be able to submit your choice until {{$a}}.",
|
||||||
|
"publishinfoanonafter": "Anonymous results will be published after you answer.",
|
||||||
|
"publishinfoanonclose": "Anonymous results will be published after the activity is closed.",
|
||||||
|
"publishinfofullafter": "Full results, showing everyone's choices, will be published after you answer.",
|
||||||
|
"publishinfofullclose": "Full results, showing everyone's choices, will be published after the activity is closed.",
|
||||||
|
"publishinfonever": "The results of this activity will not be published after you answer.",
|
||||||
|
"removemychoice": "Remove my choice",
|
||||||
|
"responses": "Responses",
|
||||||
|
"responsesa": "Responses: {{$a}}",
|
||||||
|
"responsesresultgraphdescription": "{{number}}% of the users chose the option: {{text}}.",
|
||||||
|
"responsesresultgraphheader": "Graph display",
|
||||||
|
"resultsnotsynced": "Your last response must be synchronised before it is included in the results.",
|
||||||
|
"savemychoice": "Save my choice",
|
||||||
|
"userchoosethisoption": "Users who chose this option",
|
||||||
|
"yourselection": "Your selection"
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
<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]="!activityComponent?.loaded" (ionRefresh)="activityComponent?.doRefresh($event.target)">
|
||||||
|
<ion-refresher-content pullingText="{{ 'core.pulltorefresh' | translate }}"></ion-refresher-content>
|
||||||
|
</ion-refresher>
|
||||||
|
|
||||||
|
<addon-mod-choice-index [module]="module" [courseId]="courseId" (dataRetrieved)="updateData($event)"></addon-mod-choice-index>
|
||||||
|
</ion-content>
|
|
@ -0,0 +1,30 @@
|
||||||
|
// (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, ViewChild } from '@angular/core';
|
||||||
|
import { CoreCourseModuleMainActivityPage } from '@features/course/classes/main-activity-page';
|
||||||
|
import { AddonModChoiceIndexComponent } from '../../components/index/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Page that displays a choice.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'page-addon-mod-choice-index',
|
||||||
|
templateUrl: 'index.html',
|
||||||
|
})
|
||||||
|
export class AddonModChoiceIndexPage extends CoreCourseModuleMainActivityPage<AddonModChoiceIndexComponent> {
|
||||||
|
|
||||||
|
@ViewChild(AddonModChoiceIndexComponent) activityComponent?: AddonModChoiceIndexComponent;
|
||||||
|
|
||||||
|
}
|
|
@ -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 { Injectable } from '@angular/core';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonModChoiceResponsesDBRecord, RESPONSES_TABLE_NAME } from './database/choice';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to handle offline choices.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModChoiceOfflineProvider {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a response.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID to remove.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @param userId User the responses belong to. If not defined, current user in site.
|
||||||
|
* @return Promise resolved if stored, rejected if failure.
|
||||||
|
*/
|
||||||
|
async deleteResponse(choiceId: number, siteId?: string, userId?: number): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
userId = userId || site.getUserId();
|
||||||
|
|
||||||
|
await site.getDb().deleteRecords(RESPONSES_TABLE_NAME, { choiceid: choiceId, userid: userId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all offline responses.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promi[se resolved with responses.
|
||||||
|
*/
|
||||||
|
async getResponses(siteId?: string): Promise<AddonModChoiceOfflineResponses[]> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const records = await site.getDb().getRecords<AddonModChoiceResponsesDBRecord>(RESPONSES_TABLE_NAME);
|
||||||
|
|
||||||
|
return records.map((record) => this.parseResponse(record));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if there are offline responses to send.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @param userId User the responses belong to. If not defined, current user in site.
|
||||||
|
* @return Promise resolved with boolean: true if has offline answers, false otherwise.
|
||||||
|
*/
|
||||||
|
async hasResponse(choiceId: number, siteId?: string, userId?: number): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
const response = await this.getResponse(choiceId, siteId, userId);
|
||||||
|
|
||||||
|
return !!response.choiceid;
|
||||||
|
} catch (error) {
|
||||||
|
// No offline data found, return false.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get response to be synced.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID to get.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @param userId User the responses belong to. If not defined, current user in site.
|
||||||
|
* @return Promise resolved with the object to be synced.
|
||||||
|
*/
|
||||||
|
async getResponse(choiceId: number, siteId?: string, userId?: number): Promise<AddonModChoiceOfflineResponses> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
userId = userId || site.getUserId();
|
||||||
|
|
||||||
|
const record = await site.getDb().getRecord<AddonModChoiceResponsesDBRecord>(RESPONSES_TABLE_NAME, {
|
||||||
|
choiceid: choiceId,
|
||||||
|
userid: userId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.parseResponse(record);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse responses.
|
||||||
|
*
|
||||||
|
* @param entry Entry to parse.
|
||||||
|
* @return Parsed entry.
|
||||||
|
*/
|
||||||
|
protected parseResponse(entry: AddonModChoiceResponsesDBRecord): AddonModChoiceOfflineResponses {
|
||||||
|
return {
|
||||||
|
...entry,
|
||||||
|
responses: CoreTextUtils.parseJSON(entry.responses, <number[]> []),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Offline version for sending a response to a choice to Moodle.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param name Choice name.
|
||||||
|
* @param courseId Course ID the choice belongs to.
|
||||||
|
* @param responses IDs of selected options.
|
||||||
|
* @param deleting If true, the user is deleting responses, if false, submitting.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @param userId User the responses belong to. If not defined, current user in site.
|
||||||
|
* @return Promise resolved when results are successfully submitted.
|
||||||
|
*/
|
||||||
|
async saveResponse(
|
||||||
|
choiceId: number,
|
||||||
|
name: string,
|
||||||
|
courseId: number,
|
||||||
|
responses: number[],
|
||||||
|
deleting: boolean,
|
||||||
|
siteId?: string,
|
||||||
|
userId?: number,
|
||||||
|
): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const entry: AddonModChoiceResponsesDBRecord = {
|
||||||
|
choiceid: choiceId,
|
||||||
|
name: name,
|
||||||
|
courseid: courseId,
|
||||||
|
userid: userId || site.getUserId(),
|
||||||
|
responses: JSON.stringify(responses),
|
||||||
|
deleting: deleting ? 1 : 0,
|
||||||
|
timecreated: Date.now(),
|
||||||
|
};
|
||||||
|
|
||||||
|
await site.getDb().insertRecord(RESPONSES_TABLE_NAME, entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModChoiceOffline = makeSingleton(AddonModChoiceOfflineProvider);
|
||||||
|
|
||||||
|
export type AddonModChoiceOfflineResponses = Omit<AddonModChoiceResponsesDBRecord, 'responses'> & {
|
||||||
|
responses: number[];
|
||||||
|
};
|
|
@ -0,0 +1,236 @@
|
||||||
|
// (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 { CoreNetworkError } from '@classes/errors/network-error';
|
||||||
|
import { CoreCourseActivitySyncBaseProvider } from '@features/course/classes/activity-sync';
|
||||||
|
import { CoreCourse } from '@features/course/services/course';
|
||||||
|
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreSites } from '@services/sites';
|
||||||
|
import { CoreTextUtils } from '@services/utils/text';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { makeSingleton, Translate } from '@singletons';
|
||||||
|
import { CoreEvents } from '@singletons/events';
|
||||||
|
import { AddonModChoice, AddonModChoiceProvider } from './choice';
|
||||||
|
import { AddonModChoiceOffline } from './choice-offline';
|
||||||
|
import { AddonModChoicePrefetchHandler } from './handlers/prefetch';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to sync choices.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModChoiceSyncProvider extends CoreCourseActivitySyncBaseProvider<AddonModChoiceSyncResult> {
|
||||||
|
|
||||||
|
static readonly AUTO_SYNCED = 'addon_mod_choice_autom_synced';
|
||||||
|
|
||||||
|
protected componentTranslatableString = 'choice';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('AddonModChoiceSyncProvider');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of a choice sync.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param userId User the responses belong to.
|
||||||
|
* @return Sync ID.
|
||||||
|
*/
|
||||||
|
protected getSyncId(choiceId: number, userId: number): string {
|
||||||
|
return choiceId + '#' + userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Try to synchronize all the choices in a certain site or in all sites.
|
||||||
|
*
|
||||||
|
* @param siteId Site ID to sync. If not defined, sync all sites.
|
||||||
|
* @param force Wether to force sync not depending on last execution.
|
||||||
|
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
syncAllChoices(siteId?: string, force?: boolean): Promise<void> {
|
||||||
|
return this.syncOnSites('all choices', this.syncAllChoicesFunc.bind(this, !!force), siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync all pending choices on a site.
|
||||||
|
*
|
||||||
|
* @param force Wether to force sync not depending on last execution.
|
||||||
|
* @param siteId Site ID to sync.
|
||||||
|
* @return Promise resolved if sync is successful, rejected if sync fails.
|
||||||
|
*/
|
||||||
|
protected async syncAllChoicesFunc(force: boolean, siteId: string): Promise<void> {
|
||||||
|
const responses = await AddonModChoiceOffline.getResponses(siteId);
|
||||||
|
|
||||||
|
// Sync all responses.
|
||||||
|
await Promise.all(responses.map(async (response) => {
|
||||||
|
const result = force ?
|
||||||
|
await this.syncChoice(response.choiceid, response.userid, siteId) :
|
||||||
|
await this.syncChoiceIfNeeded(response.choiceid, response.userid, siteId);
|
||||||
|
|
||||||
|
if (result?.updated) {
|
||||||
|
// Sync successful, send event.
|
||||||
|
CoreEvents.trigger(AddonModChoiceSyncProvider.AUTO_SYNCED, {
|
||||||
|
choiceId: response.choiceid,
|
||||||
|
userId: response.userid,
|
||||||
|
warnings: result.warnings,
|
||||||
|
}, siteId);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync an choice only if a certain time has passed since the last time.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID to be synced.
|
||||||
|
* @param userId User the answers belong to.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the choice is synced or it doesn't need to be synced.
|
||||||
|
*/
|
||||||
|
async syncChoiceIfNeeded(choiceId: number, userId: number, siteId?: string): Promise<AddonModChoiceSyncResult | undefined> {
|
||||||
|
const syncId = this.getSyncId(choiceId, userId);
|
||||||
|
|
||||||
|
const needed = await this.isSyncNeeded(syncId, siteId);
|
||||||
|
|
||||||
|
if (needed) {
|
||||||
|
return this.syncChoice(choiceId, userId, siteId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize a choice.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID to be synced.
|
||||||
|
* @param userId User the answers belong to.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||||
|
*/
|
||||||
|
async syncChoice(choiceId: number, userId?: number, siteId?: string): Promise<AddonModChoiceSyncResult> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
userId = userId || site.getUserId();
|
||||||
|
siteId = site.getId();
|
||||||
|
|
||||||
|
const syncId = this.getSyncId(choiceId, userId);
|
||||||
|
if (this.isSyncing(syncId, siteId)) {
|
||||||
|
// There's already a sync ongoing for this discussion, return the promise.
|
||||||
|
return this.getOngoingSync(syncId, siteId)!;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Try to sync choice '${choiceId}' for user '${userId}'`);
|
||||||
|
|
||||||
|
return this.addOngoingSync(syncId, this.performSync(choiceId, userId, siteId), siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronize a choice.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID to be synced.
|
||||||
|
* @param userId User the answers belong to.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved if sync is successful, rejected otherwise.
|
||||||
|
*/
|
||||||
|
protected async performSync(choiceId: number, userId: number, siteId?: string): Promise<AddonModChoiceSyncResult> {
|
||||||
|
const syncId = this.getSyncId(choiceId, userId);
|
||||||
|
const result: AddonModChoiceSyncResult = {
|
||||||
|
warnings: [],
|
||||||
|
updated: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Sync offline logs.
|
||||||
|
await CoreUtils.ignoreErrors(CoreCourseLogHelper.syncActivity(AddonModChoiceProvider.COMPONENT, choiceId, siteId));
|
||||||
|
|
||||||
|
const data = await CoreUtils.ignoreErrors(AddonModChoiceOffline.getResponse(choiceId, siteId, userId));
|
||||||
|
|
||||||
|
if (!data || !data.choiceid) {
|
||||||
|
// Nothing to sync. Set sync time.
|
||||||
|
await this.setSyncTime(syncId, siteId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CoreApp.isOnline()) {
|
||||||
|
// Cannot sync in offline.
|
||||||
|
throw new CoreNetworkError();
|
||||||
|
}
|
||||||
|
|
||||||
|
const courseId = data.courseid;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send the responses.
|
||||||
|
if (data.deleting) {
|
||||||
|
// A user has deleted some responses.
|
||||||
|
await AddonModChoice.deleteResponsesOnline(choiceId, data.responses, siteId);
|
||||||
|
} else {
|
||||||
|
// A user has added some responses.
|
||||||
|
await AddonModChoice.submitResponseOnline(choiceId, data.responses, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
await AddonModChoiceOffline.deleteResponse(choiceId, siteId, userId);
|
||||||
|
} catch (error) {
|
||||||
|
if (!CoreUtils.isWebServiceError(error)) {
|
||||||
|
// Couldn't connect to server, reject.
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
|
||||||
|
result.updated = true;
|
||||||
|
|
||||||
|
await AddonModChoiceOffline.deleteResponse(choiceId, siteId, userId);
|
||||||
|
|
||||||
|
// Responses deleted, add a warning.
|
||||||
|
result.warnings.push(Translate.instant('core.warningofflinedatadeleted', {
|
||||||
|
component: this.componentTranslate,
|
||||||
|
name: data.name,
|
||||||
|
error: CoreTextUtils.getErrorMessageFromError(error),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Data has been sent to server, prefetch choice if needed.
|
||||||
|
try {
|
||||||
|
const module = await CoreCourse.getModuleBasicInfoByInstance(choiceId, 'choice', siteId);
|
||||||
|
|
||||||
|
await this.prefetchAfterUpdate(AddonModChoicePrefetchHandler.instance, module, courseId, undefined, siteId);
|
||||||
|
} catch {
|
||||||
|
// Ignore errors.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync finished, set sync time.
|
||||||
|
await this.setSyncTime(syncId, siteId);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModChoiceSync = makeSingleton(AddonModChoiceSyncProvider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by a choice sync.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceSyncResult = {
|
||||||
|
warnings: string[]; // List of warnings.
|
||||||
|
updated: boolean; // Whether some data was sent to the server or offline data was updated.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data passed to AUTO_SYNCED event.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceAutoSyncData = {
|
||||||
|
choiceId: number;
|
||||||
|
userId: number;
|
||||||
|
warnings: string[];
|
||||||
|
};
|
|
@ -0,0 +1,603 @@
|
||||||
|
// (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 { CoreError } from '@classes/errors/error';
|
||||||
|
import { CoreWSError } from '@classes/errors/wserror';
|
||||||
|
import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
|
||||||
|
import { CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
||||||
|
import { CoreCourseLogHelper } from '@features/course/services/log-helper';
|
||||||
|
import { CoreApp } from '@services/app';
|
||||||
|
import { CoreFilepool } from '@services/filepool';
|
||||||
|
import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreStatusWithWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonModChoiceOffline } from './choice-offline';
|
||||||
|
import { AddonModChoiceAutoSyncData, AddonModChoiceSyncProvider } from './choice-sync';
|
||||||
|
|
||||||
|
const ROOT_CACHE_KEY = 'mmaModChoice:';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that provides some features for choices.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModChoiceProvider {
|
||||||
|
|
||||||
|
static readonly COMPONENT = 'mmaModChoice';
|
||||||
|
|
||||||
|
static readonly RESULTS_NOT = 0;
|
||||||
|
static readonly RESULTS_AFTER_ANSWER = 1;
|
||||||
|
static readonly RESULTS_AFTER_CLOSE = 2;
|
||||||
|
static readonly RESULTS_ALWAYS = 3;
|
||||||
|
|
||||||
|
static readonly PUBLISH_ANONYMOUS = false;
|
||||||
|
static readonly PUBLISH_NAMES = true;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if results can be seen by a student. The student can see the results if:
|
||||||
|
* - they're always published, OR
|
||||||
|
* - they're published after the choice is closed and it's closed, OR
|
||||||
|
* - they're published after answering and the user has answered.
|
||||||
|
*
|
||||||
|
* @param choice Choice to check.
|
||||||
|
* @param hasAnswered True if user has answered the choice, false otherwise.
|
||||||
|
* @return True if the students can see the results.
|
||||||
|
*/
|
||||||
|
canStudentSeeResults(choice: AddonModChoiceChoice, hasAnswered: boolean): boolean {
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
return choice.showresults === AddonModChoiceProvider.RESULTS_ALWAYS ||
|
||||||
|
choice.showresults === AddonModChoiceProvider.RESULTS_AFTER_CLOSE && choice.timeclose && choice.timeclose <= now ||
|
||||||
|
choice.showresults === AddonModChoiceProvider.RESULTS_AFTER_ANSWER && hasAnswered;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete responses from a choice.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param name Choice name.
|
||||||
|
* @param courseId Course ID the choice belongs to.
|
||||||
|
* @param responses IDs of the answers. If not defined, delete all the answers of the current user.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with boolean: true if response was sent to server, false if stored in device.
|
||||||
|
*/
|
||||||
|
async deleteResponses(
|
||||||
|
choiceId: number,
|
||||||
|
name: string,
|
||||||
|
courseId: number,
|
||||||
|
responses?: number[],
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
responses = responses || [];
|
||||||
|
|
||||||
|
// Convenience function to store a message to be synchronized later.
|
||||||
|
const storeOffline = async (): Promise<boolean> => {
|
||||||
|
await AddonModChoiceOffline.saveResponse(choiceId, name, courseId, responses!, true, siteId);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!CoreApp.isOnline()) {
|
||||||
|
// App is offline, store the action.
|
||||||
|
return storeOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's already a response to be sent to the server, discard it first.
|
||||||
|
await AddonModChoiceOffline.deleteResponse(choiceId, siteId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.deleteResponsesOnline(choiceId, responses, siteId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (CoreUtils.isWebServiceError(error)) {
|
||||||
|
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Couldn't connect to server, store in offline.
|
||||||
|
return storeOffline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete responses from a choice. It will fail if offline or cannot connect.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param responses IDs of the answers. If not defined, delete all the answers of the current user.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when responses are successfully deleted.
|
||||||
|
*/
|
||||||
|
async deleteResponsesOnline(choiceId: number, responses?: number[], siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const params: AddonModChoiceDeleteChoiceResponsesWSParams = {
|
||||||
|
choiceid: choiceId,
|
||||||
|
responses: responses,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await site.write<CoreStatusWithWarningsWSResponse>('mod_choice_delete_choice_responses', params);
|
||||||
|
|
||||||
|
// Other errors ocurring.
|
||||||
|
if (response.status === false) {
|
||||||
|
if (response.warnings?.[0]) {
|
||||||
|
throw new CoreWSError(response.warnings[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CoreError('Cannot delete responses.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalidate related data.
|
||||||
|
await CoreUtils.ignoreErrors(Promise.all([
|
||||||
|
this.invalidateOptions(choiceId, site.id),
|
||||||
|
this.invalidateResults(choiceId, site.id),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for choice data WS calls.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getChoiceDataCacheKey(courseId: number): string {
|
||||||
|
return ROOT_CACHE_KEY + 'choice:' + courseId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for choice options WS calls.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getChoiceOptionsCacheKey(choiceId: number): string {
|
||||||
|
return ROOT_CACHE_KEY + 'options:' + choiceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get cache key for choice results WS calls.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @return Cache key.
|
||||||
|
*/
|
||||||
|
protected getChoiceResultsCacheKey(choiceId: number): string {
|
||||||
|
return ROOT_CACHE_KEY + 'results:' + choiceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a choice with key=value. If more than one is found, only the first will be returned.
|
||||||
|
*
|
||||||
|
* @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 choice is retrieved.
|
||||||
|
*/
|
||||||
|
protected async getChoiceByDataKey(
|
||||||
|
courseId: number,
|
||||||
|
key: string,
|
||||||
|
value: unknown,
|
||||||
|
options: CoreSitesCommonWSOptions = {},
|
||||||
|
): Promise<AddonModChoiceChoice> {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
const params: AddonModChoiceGetChoicesByCoursesWSParams = {
|
||||||
|
courseids: [courseId],
|
||||||
|
};
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getChoiceDataCacheKey(courseId),
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||||
|
component: AddonModChoiceProvider.COMPONENT,
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await site.read<AddonModChoiceGetChoicesByCoursesWSResponse>(
|
||||||
|
'mod_choice_get_choices_by_courses',
|
||||||
|
params,
|
||||||
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentChoice = response.choices.find((choice) => choice[key] == value);
|
||||||
|
if (currentChoice) {
|
||||||
|
return currentChoice;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CoreError('Choice not found.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a choice by course module ID.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param cmId Course module ID.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved when the choice is retrieved.
|
||||||
|
*/
|
||||||
|
getChoice(courseId: number, cmId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModChoiceChoice> {
|
||||||
|
return this.getChoiceByDataKey(courseId, 'coursemodule', cmId, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a choice by ID.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved when the choice is retrieved.
|
||||||
|
*/
|
||||||
|
getChoiceById(courseId: number, choiceId: number, options: CoreSitesCommonWSOptions = {}): Promise<AddonModChoiceChoice> {
|
||||||
|
return this.getChoiceByDataKey(courseId, 'id', choiceId, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get choice options.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved with choice options.
|
||||||
|
*/
|
||||||
|
async getOptions(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChoiceOption[]> {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
const params: AddonModChoiceGetChoiceOptionsWSParams = {
|
||||||
|
choiceid: choiceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getChoiceOptionsCacheKey(choiceId),
|
||||||
|
updateFrequency: CoreSite.FREQUENCY_RARELY,
|
||||||
|
component: AddonModChoiceProvider.COMPONENT,
|
||||||
|
componentId: options.cmId,
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await site.read<AddonModChoiceGetChoiceOptionsWSResponse>(
|
||||||
|
'mod_choice_get_choice_options',
|
||||||
|
params,
|
||||||
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get choice results.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param options Other options.
|
||||||
|
* @return Promise resolved with choice results.
|
||||||
|
*/
|
||||||
|
async getResults(choiceId: number, options: CoreCourseCommonModWSOptions = {}): Promise<AddonModChoiceResult[]> {
|
||||||
|
const site = await CoreSites.getSite(options.siteId);
|
||||||
|
|
||||||
|
const params: AddonModChoiceGetChoiceResultsWSParams = {
|
||||||
|
choiceid: choiceId,
|
||||||
|
};
|
||||||
|
const preSets: CoreSiteWSPreSets = {
|
||||||
|
cacheKey: this.getChoiceOptionsCacheKey(choiceId),
|
||||||
|
component: AddonModChoiceProvider.COMPONENT,
|
||||||
|
componentId: options.cmId,
|
||||||
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy), // Include reading strategy preSets.
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = await site.read<AddonModChoiceGetChoiceResultsWSResponse>(
|
||||||
|
'mod_choice_get_choice_results',
|
||||||
|
params,
|
||||||
|
preSets,
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate choice data.
|
||||||
|
*
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateChoiceData(courseId: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getChoiceDataCacheKey(courseId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate the prefetched content.
|
||||||
|
*
|
||||||
|
* @param moduleId The module ID.
|
||||||
|
* @param courseId Course ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
const choice = await this.getChoice(courseId, moduleId);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
this.invalidateChoiceData(courseId),
|
||||||
|
this.invalidateOptions(choice.id),
|
||||||
|
this.invalidateResults(choice.id),
|
||||||
|
CoreFilepool.invalidateFilesByComponent(siteId, AddonModChoiceProvider.COMPONENT, moduleId),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate choice options.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateOptions(choiceId: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getChoiceOptionsCacheKey(choiceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Invalidate choice results.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the data is invalidated.
|
||||||
|
*/
|
||||||
|
async invalidateResults(choiceId: number, siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
await site.invalidateWsCacheForKey(this.getChoiceResultsCacheKey(choiceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Report the choice as being viewed.
|
||||||
|
*
|
||||||
|
* @param id Choice ID.
|
||||||
|
* @param name Name of the choice.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when the WS call is successful.
|
||||||
|
*/
|
||||||
|
logView(id: number, name?: string, siteId?: string): Promise<void> {
|
||||||
|
const params: AddonModChoiceViewChoiceWSParams = {
|
||||||
|
choiceid: id,
|
||||||
|
};
|
||||||
|
|
||||||
|
return CoreCourseLogHelper.logSingle(
|
||||||
|
'mod_choice_view_choice',
|
||||||
|
params,
|
||||||
|
AddonModChoiceProvider.COMPONENT,
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
'choice',
|
||||||
|
{},
|
||||||
|
siteId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a response to a choice to Moodle.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param name Choice name.
|
||||||
|
* @param courseId Course ID the choice belongs to.
|
||||||
|
* @param responses IDs of selected options.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved with boolean: true if response was sent to server, false if stored in device.
|
||||||
|
*/
|
||||||
|
async submitResponse(choiceId: number, name: string, courseId: number, responses: number[], siteId?: string): Promise<boolean> {
|
||||||
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
// Convenience function to store a message to be synchronized later.
|
||||||
|
const storeOffline = async (): Promise<boolean> => {
|
||||||
|
await AddonModChoiceOffline.saveResponse(choiceId, name, courseId, responses, false, siteId);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!CoreApp.isOnline()) {
|
||||||
|
// App is offline, store the action.
|
||||||
|
return storeOffline();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there's already a response to be sent to the server, discard it first.
|
||||||
|
await AddonModChoiceOffline.deleteResponse(choiceId, siteId);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.submitResponseOnline(choiceId, responses, siteId);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
if (CoreUtils.isWebServiceError(error)) {
|
||||||
|
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Couldn't connect to server, store it offline.
|
||||||
|
return storeOffline();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a response to a choice to Moodle. It will fail if offline or cannot connect.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param responses IDs of selected options.
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when responses are successfully submitted.
|
||||||
|
*/
|
||||||
|
async submitResponseOnline(choiceId: number, responses: number[], siteId?: string): Promise<void> {
|
||||||
|
const site = await CoreSites.getSite(siteId);
|
||||||
|
|
||||||
|
const params: AddonModChoiceSubmitChoiceResponseWSParams = {
|
||||||
|
choiceid: choiceId,
|
||||||
|
responses: responses,
|
||||||
|
};
|
||||||
|
|
||||||
|
await site.write('mod_choice_submit_choice_response', params);
|
||||||
|
|
||||||
|
// Invalidate related data.
|
||||||
|
await CoreUtils.ignoreErrors(Promise.all([
|
||||||
|
this.invalidateOptions(choiceId, siteId),
|
||||||
|
this.invalidateResults(choiceId, siteId),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModChoice = makeSingleton(AddonModChoiceProvider);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_choice_get_choices_by_courses WS.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceGetChoicesByCoursesWSParams = {
|
||||||
|
courseids?: number[]; // Array of course ids.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by mod_choice_get_choices_by_courses WS.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceGetChoicesByCoursesWSResponse = {
|
||||||
|
choices: AddonModChoiceChoice[];
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Choice returned by mod_choice_get_choices_by_courses.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceChoice = {
|
||||||
|
id: number; // Choice instance id.
|
||||||
|
coursemodule: number; // Course module id.
|
||||||
|
course: number; // Course id.
|
||||||
|
name: string; // Choice name.
|
||||||
|
intro: string; // The choice intro.
|
||||||
|
introformat: number; // Intro format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
|
||||||
|
introfiles?: CoreWSExternalFile[]; // @since 3.2.
|
||||||
|
publish?: boolean; // If choice is published.
|
||||||
|
showresults?: number; // 0 never, 1 after answer, 2 after close, 3 always.
|
||||||
|
display?: number; // Display mode (vertical, horizontal).
|
||||||
|
allowupdate?: boolean; // Allow update.
|
||||||
|
allowmultiple?: boolean; // Allow multiple choices.
|
||||||
|
showunanswered?: boolean; // Show users who not answered yet.
|
||||||
|
includeinactive?: boolean; // Include inactive users.
|
||||||
|
limitanswers?: boolean; // Limit unswers.
|
||||||
|
timeopen?: number; // Date of opening validity.
|
||||||
|
timeclose?: number; // Date of closing validity.
|
||||||
|
showpreview?: boolean; // Show preview before timeopen.
|
||||||
|
timemodified?: number; // Time of last modification.
|
||||||
|
completionsubmit?: boolean; // Completion on user submission.
|
||||||
|
showavailable?: boolean; // Show available spaces. @since 3.10
|
||||||
|
section?: number; // Course section id.
|
||||||
|
visible?: boolean; // Visible.
|
||||||
|
groupmode?: number; // Group mode.
|
||||||
|
groupingid?: number; // Group id.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_choice_delete_choice_responses WS.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceDeleteChoiceResponsesWSParams = {
|
||||||
|
choiceid: number; // Choice instance id.
|
||||||
|
responses?: number[]; // Array of response ids, empty for deleting all the current user responses.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_choice_get_choice_options WS.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceGetChoiceOptionsWSParams = {
|
||||||
|
choiceid: number; // Choice instance id.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by mod_choice_get_choice_options WS.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceGetChoiceOptionsWSResponse = {
|
||||||
|
options: AddonModChoiceOption[]; // Options.
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option returned by mod_choice_get_choice_options.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceOption = {
|
||||||
|
id: number; // Option id.
|
||||||
|
text: string; // Text of the choice.
|
||||||
|
maxanswers: number; // Maximum number of answers.
|
||||||
|
displaylayout: boolean; // True for orizontal, otherwise vertical.
|
||||||
|
countanswers: number; // Number of answers.
|
||||||
|
checked: boolean; // We already answered.
|
||||||
|
disabled: boolean; // Option disabled.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_choice_get_choice_results WS.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceGetChoiceResultsWSParams = {
|
||||||
|
choiceid: number; // Choice instance id.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Data returned by mod_choice_get_choice_results WS.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceGetChoiceResultsWSResponse = {
|
||||||
|
options: AddonModChoiceResult[];
|
||||||
|
warnings?: CoreWSExternalWarning[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Result returned by mod_choice_get_choice_results.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceResult = {
|
||||||
|
id: number; // Choice instance id.
|
||||||
|
text: string; // Text of the choice.
|
||||||
|
maxanswer: number; // Maximum number of answers.
|
||||||
|
userresponses: {
|
||||||
|
userid: number; // User id.
|
||||||
|
fullname: string; // User full name.
|
||||||
|
profileimageurl: string; // Profile user image url.
|
||||||
|
answerid?: number; // Answer id.
|
||||||
|
timemodified?: number; // Time of modification.
|
||||||
|
}[];
|
||||||
|
numberofuser: number; // Number of users answers.
|
||||||
|
percentageamount: number; // Percentage of users answers.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_choice_view_choice WS.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceViewChoiceWSParams = {
|
||||||
|
choiceid: number; // Choice instance id.
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Params of mod_choice_submit_choice_response WS.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceSubmitChoiceResponseWSParams = {
|
||||||
|
choiceid: number; // Choice instance id.
|
||||||
|
responses: number[]; // Array of response ids.
|
||||||
|
};
|
||||||
|
|
||||||
|
declare module '@singletons/events' {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Augment CoreEventsData interface with events specific to this service.
|
||||||
|
*
|
||||||
|
* @see https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation
|
||||||
|
*/
|
||||||
|
export interface CoreEventsData {
|
||||||
|
[AddonModChoiceSyncProvider.AUTO_SYNCED]: AddonModChoiceAutoSyncData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
// (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 { CoreSiteSchema } from '@services/sites';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database variables for AddonModChoiceOfflineProvider.
|
||||||
|
*/
|
||||||
|
export const RESPONSES_TABLE_NAME = 'addon_mod_choice_responses';
|
||||||
|
export const OFFLINE_SITE_SCHEMA: CoreSiteSchema = {
|
||||||
|
name: 'AddonModChoiceOfflineProvider',
|
||||||
|
version: 1,
|
||||||
|
tables: [
|
||||||
|
{
|
||||||
|
name: RESPONSES_TABLE_NAME,
|
||||||
|
columns: [
|
||||||
|
{
|
||||||
|
name: 'choiceid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'name',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'courseid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'userid',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'responses',
|
||||||
|
type: 'TEXT',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'deleting',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'timecreated',
|
||||||
|
type: 'INTEGER',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primaryKeys: ['choiceid', 'userid'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Response data.
|
||||||
|
*/
|
||||||
|
export type AddonModChoiceResponsesDBRecord = {
|
||||||
|
choiceid: number;
|
||||||
|
userid: number;
|
||||||
|
courseid: number;
|
||||||
|
name: string;
|
||||||
|
responses: string;
|
||||||
|
deleting: number;
|
||||||
|
timecreated: number;
|
||||||
|
};
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to treat links to choice index.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModChoiceIndexLinkHandlerService extends CoreContentLinksModuleIndexHandler {
|
||||||
|
|
||||||
|
name = 'AddonModChoiceIndexLinkHandler';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('AddonModChoice', 'choice');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModChoiceIndexLinkHandler = makeSingleton(AddonModChoiceIndexLinkHandlerService);
|
|
@ -0,0 +1,33 @@
|
||||||
|
// (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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to treat links to choice list page.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModChoiceListLinkHandlerService extends CoreContentLinksModuleListHandler {
|
||||||
|
|
||||||
|
name = 'AddonModChoiceListLinkHandler';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('AddonModChoice', 'choice');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModChoiceListLinkHandler = makeSingleton(AddonModChoiceListLinkHandlerService);
|
|
@ -0,0 +1,83 @@
|
||||||
|
// (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 { CoreConstants } from '@/core/constants';
|
||||||
|
import { Injectable, Type } from '@angular/core';
|
||||||
|
import { CoreCourse, CoreCourseAnyModuleData } from '@features/course/services/course';
|
||||||
|
import { CoreCourseModule } from '@features/course/services/course-helper';
|
||||||
|
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@features/course/services/module-delegate';
|
||||||
|
import { CoreNavigationOptions, CoreNavigator } from '@services/navigator';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonModChoiceIndexComponent } from '../../components/index';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to support choice modules.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModChoiceModuleHandlerService implements CoreCourseModuleHandler {
|
||||||
|
|
||||||
|
static readonly PAGE_NAME = 'mod_choice';
|
||||||
|
|
||||||
|
name = 'AddonModChoice';
|
||||||
|
modName = 'choice';
|
||||||
|
|
||||||
|
supportedFeatures = {
|
||||||
|
[CoreConstants.FEATURE_GROUPS]: true,
|
||||||
|
[CoreConstants.FEATURE_GROUPINGS]: true,
|
||||||
|
[CoreConstants.FEATURE_MOD_INTRO]: true,
|
||||||
|
[CoreConstants.FEATURE_COMPLETION_TRACKS_VIEWS]: true,
|
||||||
|
[CoreConstants.FEATURE_COMPLETION_HAS_RULES]: true,
|
||||||
|
[CoreConstants.FEATURE_GRADE_HAS_GRADE]: false,
|
||||||
|
[CoreConstants.FEATURE_GRADE_OUTCOMES]: false,
|
||||||
|
[CoreConstants.FEATURE_BACKUP_MOODLE2]: true,
|
||||||
|
[CoreConstants.FEATURE_SHOW_DESCRIPTION]: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async isEnabled(): Promise<boolean> {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
getData(module: CoreCourseAnyModuleData): CoreCourseModuleHandlerData {
|
||||||
|
return {
|
||||||
|
icon: CoreCourse.getModuleIconSrc(this.modName, 'modicon' in module ? module.modicon : undefined),
|
||||||
|
title: module.name,
|
||||||
|
class: 'addon-mod_choice-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.navigateToSitePath(AddonModChoiceModuleHandlerService.PAGE_NAME + routeParams, options);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async getMainComponent(): Promise<Type<unknown>> {
|
||||||
|
return AddonModChoiceIndexComponent;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModChoiceModuleHandler = makeSingleton(AddonModChoiceModuleHandlerService);
|
|
@ -0,0 +1,157 @@
|
||||||
|
// (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 { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
|
||||||
|
import { CoreCourseAnyModuleData, CoreCourseCommonModWSOptions } from '@features/course/services/course';
|
||||||
|
import { CoreUser } from '@features/user/services/user';
|
||||||
|
import { CoreFilepool } from '@services/filepool';
|
||||||
|
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { CoreWSExternalFile } from '@services/ws';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonModChoice, AddonModChoiceProvider } from '../choice';
|
||||||
|
import { AddonModChoiceSync, AddonModChoiceSyncResult } from '../choice-sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handler to prefetch choices.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModChoicePrefetchHandlerService extends CoreCourseActivityPrefetchHandlerBase {
|
||||||
|
|
||||||
|
name = 'AddonModChoice';
|
||||||
|
modName = 'choice';
|
||||||
|
component = AddonModChoiceProvider.COMPONENT;
|
||||||
|
updatesNames = /^configuration$|^.*files$|^answers$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
prefetch(module: CoreCourseAnyModuleData, courseId?: number, single?: boolean): Promise<void> {
|
||||||
|
return this.prefetchPackage(module, courseId, this.prefetchChoice.bind(this, module, courseId, !!single));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch a choice.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to.
|
||||||
|
* @param single True if we're downloading a single module, false if we're downloading a whole section.
|
||||||
|
* @param siteId Site ID.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async prefetchChoice(
|
||||||
|
module: CoreCourseAnyModuleData,
|
||||||
|
courseId: number,
|
||||||
|
single: boolean,
|
||||||
|
siteId?: string,
|
||||||
|
): Promise<void> {
|
||||||
|
siteId = siteId || CoreSites.getCurrentSiteId();
|
||||||
|
|
||||||
|
const commonOptions = {
|
||||||
|
readingStrategy: CoreSitesReadingStrategy.OnlyNetwork,
|
||||||
|
siteId,
|
||||||
|
};
|
||||||
|
const modOptions = {
|
||||||
|
cmId: module.id,
|
||||||
|
...commonOptions, // Include all common options.
|
||||||
|
};
|
||||||
|
|
||||||
|
const choice = await AddonModChoice.getChoice(courseId, module.id, commonOptions);
|
||||||
|
|
||||||
|
// Get the intro files.
|
||||||
|
const introFiles = this.getIntroFilesFromInstance(module, choice);
|
||||||
|
|
||||||
|
await Promise.all([
|
||||||
|
AddonModChoice.getOptions(choice.id, modOptions),
|
||||||
|
this.prefetchResults(choice.id, courseId, modOptions),
|
||||||
|
CoreFilepool.addFilesToQueue(siteId, introFiles, AddonModChoiceProvider.COMPONENT, module.id),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prefetch choice results.
|
||||||
|
*
|
||||||
|
* @param choiceId Choice ID.
|
||||||
|
* @param modOptions Options.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async prefetchResults(
|
||||||
|
choiceId: number,
|
||||||
|
courseId: number,
|
||||||
|
modOptions: CoreCourseCommonModWSOptions,
|
||||||
|
): Promise<void> {
|
||||||
|
const options = await AddonModChoice.getResults(choiceId, modOptions);
|
||||||
|
|
||||||
|
// If we can see the users that answered, prefetch their profile and avatar.
|
||||||
|
const promises: Promise<unknown>[] = [];
|
||||||
|
|
||||||
|
options.forEach((option) => {
|
||||||
|
option.userresponses.forEach((response) => {
|
||||||
|
if (response.userid) {
|
||||||
|
promises.push(CoreUser.getProfile(response.userid, courseId, false, modOptions.siteId));
|
||||||
|
}
|
||||||
|
if (response.profileimageurl) {
|
||||||
|
promises.push(CoreFilepool.addToQueueByUrl(modOptions.siteId!, response.profileimageurl).catch(() => {
|
||||||
|
// Ignore failures.
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(promises);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async getIntroFiles(module: CoreCourseAnyModuleData, courseId: number): Promise<CoreWSExternalFile[]> {
|
||||||
|
const choice = await CoreUtils.ignoreErrors(AddonModChoice.getChoice(courseId, module.id));
|
||||||
|
|
||||||
|
return this.getIntroFilesFromInstance(module, choice);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
invalidateContent(moduleId: number, courseId: number): Promise<void> {
|
||||||
|
return AddonModChoice.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.
|
||||||
|
*/
|
||||||
|
invalidateModule(module: CoreCourseAnyModuleData, courseId: number): Promise<void> {
|
||||||
|
return AddonModChoice.invalidateChoiceData(courseId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync a module.
|
||||||
|
*
|
||||||
|
* @param module Module.
|
||||||
|
* @param courseId Course ID the module belongs to
|
||||||
|
* @param siteId Site ID. If not defined, current site.
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
sync(module: CoreCourseAnyModuleData, courseId: number, siteId?: string): Promise<AddonModChoiceSyncResult> {
|
||||||
|
return AddonModChoiceSync.syncChoice(module.instance!, undefined, siteId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModChoicePrefetchHandler = makeSingleton(AddonModChoicePrefetchHandlerService);
|
|
@ -0,0 +1,51 @@
|
||||||
|
// (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 { CoreCronHandler } from '@services/cron';
|
||||||
|
import { makeSingleton } from '@singletons';
|
||||||
|
import { AddonModChoiceSync } from '../choice-sync';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronization cron handler.
|
||||||
|
*/
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AddonModChoiceSyncCronHandlerService implements CoreCronHandler {
|
||||||
|
|
||||||
|
name = 'AddonModChoiceSyncCronHandler';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the process.
|
||||||
|
* Receives the ID of the site affected, undefined for all sites.
|
||||||
|
*
|
||||||
|
* @param siteId ID of the site affected, undefined for all sites.
|
||||||
|
* @param force Wether the execution is forced (manual sync).
|
||||||
|
* @return Promise resolved when done, rejected if failure.
|
||||||
|
*/
|
||||||
|
execute(siteId?: string, force?: boolean): Promise<void> {
|
||||||
|
return AddonModChoiceSync.syncAllChoices(siteId, force);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the time between consecutive executions.
|
||||||
|
*
|
||||||
|
* @return Time between consecutive executions (in ms).
|
||||||
|
*/
|
||||||
|
getInterval(): number {
|
||||||
|
return AddonModChoiceSync.syncInterval;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AddonModChoiceSyncCronHandler = makeSingleton(AddonModChoiceSyncCronHandlerService);
|
|
@ -29,6 +29,7 @@ import { AddonModLtiModule } from './lti/lti.module';
|
||||||
import { AddonModH5PActivityModule } from './h5pactivity/h5pactivity.module';
|
import { AddonModH5PActivityModule } from './h5pactivity/h5pactivity.module';
|
||||||
import { AddonModSurveyModule } from './survey/survey.module';
|
import { AddonModSurveyModule } from './survey/survey.module';
|
||||||
import { AddonModScormModule } from './scorm/scorm.module';
|
import { AddonModScormModule } from './scorm/scorm.module';
|
||||||
|
import { AddonModChoiceModule } from './choice/choice.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [],
|
declarations: [],
|
||||||
|
@ -48,6 +49,7 @@ import { AddonModScormModule } from './scorm/scorm.module';
|
||||||
AddonModH5PActivityModule,
|
AddonModH5PActivityModule,
|
||||||
AddonModSurveyModule,
|
AddonModSurveyModule,
|
||||||
AddonModScormModule,
|
AddonModScormModule,
|
||||||
|
AddonModChoiceModule,
|
||||||
],
|
],
|
||||||
providers: [],
|
providers: [],
|
||||||
exports: [],
|
exports: [],
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
// (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 { Type } from '@angular/core';
|
||||||
|
|
||||||
|
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';
|
||||||
|
|
||||||
|
export const CORE_ERRORS_CLASSES: Type<unknown>[] = [
|
||||||
|
CoreAjaxError,
|
||||||
|
CoreAjaxWSError,
|
||||||
|
CoreCanceledError,
|
||||||
|
CoreCaptureError,
|
||||||
|
CoreError,
|
||||||
|
CoreNetworkError,
|
||||||
|
CoreSilentError,
|
||||||
|
CoreSiteError,
|
||||||
|
CoreWSError,
|
||||||
|
];
|
|
@ -0,0 +1,8 @@
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,189 @@
|
||||||
|
// (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, Input, OnDestroy, OnInit, ElementRef, OnChanges, ViewChild, SimpleChange } from '@angular/core';
|
||||||
|
import { CoreFilter } from '@features/filter/services/filter';
|
||||||
|
import { CoreFilterHelper } from '@features/filter/services/filter-helper';
|
||||||
|
import { CoreUtils } from '@services/utils/utils';
|
||||||
|
import { Chart, ChartLegendLabelItem, ChartLegendOptions } from 'chart.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component shows a chart using chart.js.
|
||||||
|
* Documentation can be found at http://www.chartjs.org/docs/.
|
||||||
|
* It only supports changes on these properties: data and labels.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
* <core-chart [data]="data" [labels]="labels" [type]="type" [legend]="legend"></core-chart>
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'core-chart',
|
||||||
|
templateUrl: 'core-chart.html',
|
||||||
|
styleUrls: ['chart.scss'],
|
||||||
|
})
|
||||||
|
export class CoreChartComponent implements OnDestroy, OnInit, OnChanges {
|
||||||
|
|
||||||
|
// The first 6 colors will be the app colors, the following will be randomly generated.
|
||||||
|
// It will use the same colors in the whole session.
|
||||||
|
protected static backgroundColors = [
|
||||||
|
'rgba(0,100,210, 0.6)',
|
||||||
|
'rgba(203,61,77, 0.6)',
|
||||||
|
'rgba(0,121,130, 0.6)',
|
||||||
|
'rgba(249,128,18, 0.6)',
|
||||||
|
'rgba(94,129,0, 0.6)',
|
||||||
|
'rgba(251,173,26, 0.6)',
|
||||||
|
];
|
||||||
|
|
||||||
|
@Input() data: number[] = []; // Chart data.
|
||||||
|
@Input() labels: string[] = []; // Labels of the data.
|
||||||
|
@Input() type?: string; // Type of chart.
|
||||||
|
@Input() legend?: ChartLegendOptions; // Legend options.
|
||||||
|
@Input() height = 300; // Height of the chart element.
|
||||||
|
@Input() filter?: boolean | string; // Whether to filter labels. If not defined, true if contextLevel and instanceId are set.
|
||||||
|
@Input() contextLevel?: string; // The context level of the text.
|
||||||
|
@Input() contextInstanceId?: number; // The instance ID related to the context.
|
||||||
|
@Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters.
|
||||||
|
@Input() wsNotFiltered?: boolean | string; // If true it means the WS didn't filter the labels for some reason.
|
||||||
|
@ViewChild('canvas') canvas?: ElementRef<HTMLCanvasElement>;
|
||||||
|
|
||||||
|
chart?: ChartWithLegend;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnInit(): Promise<void> {
|
||||||
|
let legend: ChartLegendOptions = {};
|
||||||
|
if (typeof this.legend == 'undefined') {
|
||||||
|
legend = {
|
||||||
|
display: false,
|
||||||
|
labels: {
|
||||||
|
generateLabels: (chart: Chart): ChartLegendLabelItem[] => {
|
||||||
|
const data = chart.data;
|
||||||
|
if (data.labels?.length) {
|
||||||
|
const datasets = data.datasets![0];
|
||||||
|
|
||||||
|
return data.labels.map((label, i) => ({
|
||||||
|
text: label + ': ' + datasets.data![i],
|
||||||
|
fillStyle: datasets.backgroundColor![i],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
legend = Object.assign({}, this.legend);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.type == 'bar' && this.data.length >= 5) {
|
||||||
|
this.type = 'horizontalBar';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Format labels if needed.
|
||||||
|
await this.formatLabels();
|
||||||
|
|
||||||
|
const context = this.canvas!.nativeElement.getContext('2d')!;
|
||||||
|
this.chart = new Chart(context, {
|
||||||
|
type: this.type,
|
||||||
|
data: {
|
||||||
|
labels: this.labels,
|
||||||
|
datasets: [{
|
||||||
|
data: this.data,
|
||||||
|
backgroundColor: this.getRandomColors(this.data.length),
|
||||||
|
}],
|
||||||
|
},
|
||||||
|
options: { legend },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
async ngOnChanges(changes: Record<string, SimpleChange>): Promise<void> {
|
||||||
|
if (!this.chart || !changes.labels || !changes.data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.labels) {
|
||||||
|
// Format labels if needed.
|
||||||
|
await this.formatLabels();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.chart.data.datasets![0] = {
|
||||||
|
data: this.data,
|
||||||
|
backgroundColor: this.getRandomColors(this.data.length),
|
||||||
|
};
|
||||||
|
this.chart.data.labels = this.labels;
|
||||||
|
this.chart.update();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format labels if needed.
|
||||||
|
*
|
||||||
|
* @return Promise resolved when done.
|
||||||
|
*/
|
||||||
|
protected async formatLabels(): Promise<void> {
|
||||||
|
if (!this.contextLevel || !this.contextInstanceId || this.filter === false) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = {
|
||||||
|
clean: true,
|
||||||
|
singleLine: true,
|
||||||
|
courseId: this.courseId,
|
||||||
|
wsNotFiltered: CoreUtils.isTrueOrOne(this.wsNotFiltered),
|
||||||
|
};
|
||||||
|
|
||||||
|
const filters = await CoreFilterHelper.getFilters(this.contextLevel, this.contextInstanceId, options);
|
||||||
|
|
||||||
|
await Promise.all(this.labels.map(async (label, i) => {
|
||||||
|
this.labels[i] = await CoreFilter.formatText(label, options, filters);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate random colors if needed.
|
||||||
|
*
|
||||||
|
* @param n Number of colors needed.
|
||||||
|
* @return Array with the number of background colors requested.
|
||||||
|
*/
|
||||||
|
protected getRandomColors(n: number): string[] {
|
||||||
|
while (CoreChartComponent.backgroundColors.length < n) {
|
||||||
|
const red = Math.floor(Math.random() * 255);
|
||||||
|
const green = Math.floor(Math.random() * 255);
|
||||||
|
const blue = Math.floor(Math.random() * 255);
|
||||||
|
CoreChartComponent.backgroundColors.push('rgba(' + red + ', ' + green + ', ' + blue + ', 0.6)');
|
||||||
|
}
|
||||||
|
|
||||||
|
return CoreChartComponent.backgroundColors.slice(0, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @inheritdoc
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.destroy();
|
||||||
|
this.chart = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// For some reason the legend property isn't defined in TS, define it ourselves.
|
||||||
|
type ChartWithLegend = Chart & {
|
||||||
|
legend?: {
|
||||||
|
legendItems?: ChartLegendLabelItem[];
|
||||||
|
};
|
||||||
|
};
|
|
@ -0,0 +1,8 @@
|
||||||
|
<canvas #canvas [attr.height]="height"></canvas>
|
||||||
|
|
||||||
|
<ion-list *ngIf="chart" inset="true">
|
||||||
|
<ion-item *ngFor="let data of chart.legend!.legendItems">
|
||||||
|
<ion-icon name="square" slot="start" [style.color]="data.fillStyle"></ion-icon>
|
||||||
|
<ion-label>{{data.text}}</ion-label>
|
||||||
|
</ion-item>
|
||||||
|
</ion-list>
|
|
@ -53,6 +53,7 @@ import { CoreFilesComponent } from './files/files';
|
||||||
import { CoreLocalFileComponent } from './local-file/local-file';
|
import { CoreLocalFileComponent } from './local-file/local-file';
|
||||||
import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
|
import { CoreBSTooltipComponent } from './bs-tooltip/bs-tooltip';
|
||||||
import { CoreSitePickerComponent } from './site-picker/site-picker';
|
import { CoreSitePickerComponent } from './site-picker/site-picker';
|
||||||
|
import { CoreChartComponent } from './chart/chart';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@ -88,6 +89,7 @@ import { CoreSitePickerComponent } from './site-picker/site-picker';
|
||||||
CoreLocalFileComponent,
|
CoreLocalFileComponent,
|
||||||
CoreBSTooltipComponent,
|
CoreBSTooltipComponent,
|
||||||
CoreSitePickerComponent,
|
CoreSitePickerComponent,
|
||||||
|
CoreChartComponent,
|
||||||
],
|
],
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
|
@ -130,6 +132,7 @@ import { CoreSitePickerComponent } from './site-picker/site-picker';
|
||||||
CoreLocalFileComponent,
|
CoreLocalFileComponent,
|
||||||
CoreBSTooltipComponent,
|
CoreBSTooltipComponent,
|
||||||
CoreSitePickerComponent,
|
CoreSitePickerComponent,
|
||||||
|
CoreChartComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class CoreComponentsModule {}
|
export class CoreComponentsModule {}
|
||||||
|
|
|
@ -69,6 +69,8 @@ export class CoreFormatTextDirective implements OnChanges {
|
||||||
@Input() contextInstanceId?: number; // The instance ID related to the context.
|
@Input() contextInstanceId?: number; // The instance ID related to the context.
|
||||||
@Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters.
|
@Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters.
|
||||||
@Input() wsNotFiltered?: boolean | string; // If true it means the WS didn't filter the text for some reason.
|
@Input() wsNotFiltered?: boolean | string; // If true it means the WS didn't filter the text for some reason.
|
||||||
|
@Input() captureLinks?: boolean; // Whether links should tried to be opened inside the app. Defaults to true.
|
||||||
|
@Input() openLinksInApp?: boolean; // Whether links should be opened in InAppBrowser.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Max height in pixels to render the content box. It should be 50 at least to make sense.
|
* Max height in pixels to render the content box. It should be 50 at least to make sense.
|
||||||
|
@ -489,7 +491,8 @@ export class CoreFormatTextDirective implements OnChanges {
|
||||||
anchors.forEach((anchor) => {
|
anchors.forEach((anchor) => {
|
||||||
// Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually.
|
// Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually.
|
||||||
const linkDir = new CoreLinkDirective(new ElementRef(anchor), this.content);
|
const linkDir = new CoreLinkDirective(new ElementRef(anchor), this.content);
|
||||||
linkDir.capture = true;
|
linkDir.capture = this.captureLinks ?? true;
|
||||||
|
linkDir.inApp = this.openLinksInApp;
|
||||||
linkDir.ngOnInit();
|
linkDir.ngOnInit();
|
||||||
|
|
||||||
this.addExternalContent(anchor);
|
this.addExternalContent(anchor);
|
||||||
|
|
|
@ -88,6 +88,7 @@ import { CoreContentLinksModuleIndexHandler } from '@features/contentlinks/class
|
||||||
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
|
import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler';
|
||||||
import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler';
|
import { CoreCourseResourcePrefetchHandlerBase } from '@features/course/classes/resource-prefetch-handler';
|
||||||
import { CoreGeolocationError, CoreGeolocationErrorReason } from '@services/geolocation';
|
import { CoreGeolocationError, CoreGeolocationErrorReason } from '@services/geolocation';
|
||||||
|
import { CORE_ERRORS_CLASSES } from '@classes/errors/errors';
|
||||||
|
|
||||||
// Import all core modules that define components, directives and pipes.
|
// Import all core modules that define components, directives and pipes.
|
||||||
import { CoreSharedModule } from '@/core/shared.module';
|
import { CoreSharedModule } from '@/core/shared.module';
|
||||||
|
@ -124,7 +125,7 @@ import { ADDON_MESSAGES_SERVICES } from '@addons/messages/messages.module';
|
||||||
import { ADDON_MOD_ASSIGN_SERVICES } from '@addons/mod/assign/assign.module';
|
import { ADDON_MOD_ASSIGN_SERVICES } from '@addons/mod/assign/assign.module';
|
||||||
import { ADDON_MOD_BOOK_SERVICES } from '@addons/mod/book/book.module';
|
import { ADDON_MOD_BOOK_SERVICES } from '@addons/mod/book/book.module';
|
||||||
// @todo import { ADDON_MOD_CHAT_SERVICES } from '@addons/mod/chat/chat.module';
|
// @todo import { ADDON_MOD_CHAT_SERVICES } from '@addons/mod/chat/chat.module';
|
||||||
// @todo import { ADDON_MOD_CHOICE_SERVICES } from '@addons/mod/choice/choice.module';
|
import { ADDON_MOD_CHOICE_SERVICES } from '@addons/mod/choice/choice.module';
|
||||||
// @todo import { ADDON_MOD_FEEDBACK_SERVICES } from '@addons/mod/feedback/feedback.module';
|
// @todo import { ADDON_MOD_FEEDBACK_SERVICES } from '@addons/mod/feedback/feedback.module';
|
||||||
import { ADDON_MOD_FOLDER_SERVICES } from '@addons/mod/folder/folder.module';
|
import { ADDON_MOD_FOLDER_SERVICES } from '@addons/mod/folder/folder.module';
|
||||||
import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module';
|
import { ADDON_MOD_FORUM_SERVICES } from '@addons/mod/forum/forum.module';
|
||||||
|
@ -289,7 +290,7 @@ export class CoreCompileProvider {
|
||||||
...ADDON_MOD_ASSIGN_SERVICES,
|
...ADDON_MOD_ASSIGN_SERVICES,
|
||||||
...ADDON_MOD_BOOK_SERVICES,
|
...ADDON_MOD_BOOK_SERVICES,
|
||||||
// @todo ...ADDON_MOD_CHAT_SERVICES,
|
// @todo ...ADDON_MOD_CHAT_SERVICES,
|
||||||
// @todo ...ADDON_MOD_CHOICE_SERVICES,
|
...ADDON_MOD_CHOICE_SERVICES,
|
||||||
// @todo ...ADDON_MOD_FEEDBACK_SERVICES,
|
// @todo ...ADDON_MOD_FEEDBACK_SERVICES,
|
||||||
...ADDON_MOD_FOLDER_SERVICES,
|
...ADDON_MOD_FOLDER_SERVICES,
|
||||||
...ADDON_MOD_FORUM_SERVICES,
|
...ADDON_MOD_FORUM_SERVICES,
|
||||||
|
@ -361,6 +362,9 @@ export class CoreCompileProvider {
|
||||||
instance['CoreSitePluginsAssignSubmissionComponent'] = CoreSitePluginsAssignSubmissionComponent;
|
instance['CoreSitePluginsAssignSubmissionComponent'] = CoreSitePluginsAssignSubmissionComponent;
|
||||||
instance['CoreGeolocationError'] = CoreGeolocationError;
|
instance['CoreGeolocationError'] = CoreGeolocationError;
|
||||||
instance['CoreGeolocationErrorReason'] = CoreGeolocationErrorReason;
|
instance['CoreGeolocationErrorReason'] = CoreGeolocationErrorReason;
|
||||||
|
CORE_ERRORS_CLASSES.forEach((classDef) => {
|
||||||
|
instance[classDef.name] = classDef;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -100,7 +100,7 @@ export class CoreSitePluginsUserProfileHandler extends CoreSitePluginsBaseHandle
|
||||||
const hash = <string> Md5.hashAsciiStr(JSON.stringify(args));
|
const hash = <string> Md5.hashAsciiStr(JSON.stringify(args));
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath(
|
CoreNavigator.navigateToSitePath(
|
||||||
`siteplugins/${this.plugin.component}/${this.handlerSchema.method}/${hash}`,
|
`siteplugins/content/${this.plugin.component}/${this.handlerSchema.method}/${hash}`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
title: this.title,
|
title: this.title,
|
||||||
|
|
|
@ -60,7 +60,7 @@ export class CoreSitePluginsOnlyTitleBlockComponent extends CoreBlockBaseCompon
|
||||||
const hash = <string> Md5.hashAsciiStr(JSON.stringify(args));
|
const hash = <string> Md5.hashAsciiStr(JSON.stringify(args));
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath(
|
CoreNavigator.navigateToSitePath(
|
||||||
`siteplugins/${handler.plugin.component}/${handler.handlerSchema.method}/${hash}`,
|
`siteplugins/content/${handler.plugin.component}/${handler.handlerSchema.method}/${hash}`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
title: this.title,
|
title: this.title,
|
||||||
|
|
|
@ -153,7 +153,7 @@ export class CoreSitePluginsPluginContentComponent implements OnInit, DoCheck {
|
||||||
args = args || {};
|
args = args || {};
|
||||||
const hash = <string> Md5.hashAsciiStr(JSON.stringify(args));
|
const hash = <string> Md5.hashAsciiStr(JSON.stringify(args));
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath(`siteplugins/${component}/${method}/${hash}`, {
|
CoreNavigator.navigateToSitePath(`siteplugins/content/${component}/${method}/${hash}`, {
|
||||||
params: {
|
params: {
|
||||||
title,
|
title,
|
||||||
args,
|
args,
|
||||||
|
|
|
@ -103,7 +103,7 @@ export class CoreSitePluginsCallWSNewContentDirective extends CoreSitePluginsCal
|
||||||
const method = this.method || this.parentContent?.method;
|
const method = this.method || this.parentContent?.method;
|
||||||
const hash = <string> Md5.hashAsciiStr(JSON.stringify(args));
|
const hash = <string> Md5.hashAsciiStr(JSON.stringify(args));
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath(`siteplugins/${component}/${method}/${hash}`, {
|
CoreNavigator.navigateToSitePath(`siteplugins/content/${component}/${method}/${hash}`, {
|
||||||
params: {
|
params: {
|
||||||
title: this.title || this.parentContent?.pageTitle,
|
title: this.title || this.parentContent?.pageTitle,
|
||||||
args,
|
args,
|
||||||
|
|
|
@ -100,7 +100,7 @@ export class CoreSitePluginsNewContentDirective implements OnInit {
|
||||||
const method = this.method || this.parentContent?.method;
|
const method = this.method || this.parentContent?.method;
|
||||||
const hash = <string> Md5.hashAsciiStr(JSON.stringify(args));
|
const hash = <string> Md5.hashAsciiStr(JSON.stringify(args));
|
||||||
|
|
||||||
CoreNavigator.navigateToSitePath(`siteplugins/${component}/${method}/${hash}`, {
|
CoreNavigator.navigateToSitePath(`siteplugins/content/${component}/${method}/${hash}`, {
|
||||||
params: {
|
params: {
|
||||||
title: this.title || this.parentContent?.pageTitle,
|
title: this.title || this.parentContent?.pageTitle,
|
||||||
args,
|
args,
|
||||||
|
|
|
@ -23,8 +23,8 @@ import { CoreSitePluginsHelper } from './services/siteplugins-helper';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
{
|
{
|
||||||
path: 'siteplugins',
|
path: 'siteplugins/content/:component/:method/:hash',
|
||||||
loadChildren: () => import('@features/siteplugins/siteplugins-lazy.module').then(m => m.CoreSitePluginsLazyModule),
|
loadChildren: () => import('./pages/plugin-page/plugin-page.module').then( m => m.CoreSitePluginsPluginPageModule),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -36,9 +36,16 @@ const courseIndexRoutes: Routes = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const moduleRoutes: Routes = [
|
||||||
|
{
|
||||||
|
path: 'siteplugins/module/:courseId/:cmId',
|
||||||
|
loadChildren: () => import('./pages/module-index/module-index.module').then( m => m.CoreSitePluginsModuleIndexPageModule),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CoreMainMenuTabRoutingModule.forChild(routes),
|
CoreMainMenuTabRoutingModule.forChild(moduleRoutes.concat(routes)),
|
||||||
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
|
CoreCourseIndexRoutingModule.forChild({ children: courseIndexRoutes }),
|
||||||
CoreMainMenuHomeRoutingModule.forChild({ children: routes }),
|
CoreMainMenuHomeRoutingModule.forChild({ children: routes }),
|
||||||
CoreSitePluginsComponentsModule,
|
CoreSitePluginsComponentsModule,
|
||||||
|
|
|
@ -334,10 +334,14 @@ export class CoreAppProvider {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.isMobile()) {
|
||||||
|
return navigator.onLine;
|
||||||
|
}
|
||||||
|
|
||||||
let online = Network.type !== null && Network.type != Network.Connection.NONE &&
|
let online = Network.type !== null && Network.type != Network.Connection.NONE &&
|
||||||
Network.type != Network.Connection.UNKNOWN;
|
Network.type != Network.Connection.UNKNOWN;
|
||||||
|
|
||||||
// Double check we are not online because we cannot rely 100% in Cordova APIs. Also, check it in browser.
|
// Double check we are not online because we cannot rely 100% in Cordova APIs.
|
||||||
if (!online && navigator.onLine) {
|
if (!online && navigator.onLine) {
|
||||||
online = true;
|
online = true;
|
||||||
}
|
}
|
||||||
|
@ -351,9 +355,7 @@ export class CoreAppProvider {
|
||||||
* @return Whether the device uses a limited connection.
|
* @return Whether the device uses a limited connection.
|
||||||
*/
|
*/
|
||||||
isNetworkAccessLimited(): boolean {
|
isNetworkAccessLimited(): boolean {
|
||||||
const type = Network.type;
|
if (!this.isMobile()) {
|
||||||
if (type === null) {
|
|
||||||
// Plugin not defined, probably in browser.
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -364,7 +366,7 @@ export class CoreAppProvider {
|
||||||
Network.Connection.CELL,
|
Network.Connection.CELL,
|
||||||
];
|
];
|
||||||
|
|
||||||
return limited.indexOf(type) > -1;
|
return limited.indexOf(Network.type) > -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -473,28 +473,39 @@ export class CoreFileProvider {
|
||||||
*
|
*
|
||||||
* @param path Relative path to the file.
|
* @param path Relative path to the file.
|
||||||
* @param format Format to read the file.
|
* @param format Format to read the file.
|
||||||
|
* @param folder Absolute path to the folder where the file is. Use it to read files outside of the app's data folder.
|
||||||
* @return Promise to be resolved when the file is read.
|
* @return Promise to be resolved when the file is read.
|
||||||
*/
|
*/
|
||||||
readFile(
|
readFile(
|
||||||
path: string,
|
path: string,
|
||||||
format?: CoreFileFormat.FORMATTEXT | CoreFileFormat.FORMATDATAURL | CoreFileFormat.FORMATBINARYSTRING,
|
format?: CoreFileFormat.FORMATTEXT | CoreFileFormat.FORMATDATAURL | CoreFileFormat.FORMATBINARYSTRING,
|
||||||
|
folder?: string,
|
||||||
): Promise<string>;
|
): Promise<string>;
|
||||||
readFile(path: string, format: CoreFileFormat.FORMATARRAYBUFFER): Promise<ArrayBuffer>;
|
readFile(path: string, format: CoreFileFormat.FORMATARRAYBUFFER, folder?: string): Promise<ArrayBuffer>;
|
||||||
readFile<T = unknown>(path: string, format: CoreFileFormat.FORMATJSON): Promise<T>;
|
readFile<T = unknown>(path: string, format: CoreFileFormat.FORMATJSON, folder?: string): Promise<T>;
|
||||||
readFile(path: string, format: CoreFileFormat = CoreFileFormat.FORMATTEXT): Promise<string | ArrayBuffer | unknown> {
|
readFile(
|
||||||
// Remove basePath if it's in the path.
|
path: string,
|
||||||
path = this.removeStartingSlash(path.replace(this.basePath, ''));
|
format: CoreFileFormat = CoreFileFormat.FORMATTEXT,
|
||||||
this.logger.debug('Read file ' + path + ' with format ' + format);
|
folder?: string,
|
||||||
|
): Promise<string | ArrayBuffer | unknown> {
|
||||||
|
if (!folder) {
|
||||||
|
folder = this.basePath;
|
||||||
|
|
||||||
|
// Remove basePath if it's in the path.
|
||||||
|
path = this.removeStartingSlash(path.replace(this.basePath, ''));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.debug(`Read file ${path} with format ${format} in folder ${folder}`);
|
||||||
|
|
||||||
switch (format) {
|
switch (format) {
|
||||||
case CoreFileFormat.FORMATDATAURL:
|
case CoreFileFormat.FORMATDATAURL:
|
||||||
return File.readAsDataURL(this.basePath, path);
|
return File.readAsDataURL(folder, path);
|
||||||
case CoreFileFormat.FORMATBINARYSTRING:
|
case CoreFileFormat.FORMATBINARYSTRING:
|
||||||
return File.readAsBinaryString(this.basePath, path);
|
return File.readAsBinaryString(folder, path);
|
||||||
case CoreFileFormat.FORMATARRAYBUFFER:
|
case CoreFileFormat.FORMATARRAYBUFFER:
|
||||||
return File.readAsArrayBuffer(this.basePath, path);
|
return File.readAsArrayBuffer(folder, path);
|
||||||
case CoreFileFormat.FORMATJSON:
|
case CoreFileFormat.FORMATJSON:
|
||||||
return File.readAsText(this.basePath, path).then((text) => {
|
return File.readAsText(folder, path).then((text) => {
|
||||||
const parsed = CoreTextUtils.parseJSON(text, null);
|
const parsed = CoreTextUtils.parseJSON(text, null);
|
||||||
|
|
||||||
if (parsed == null && text != null) {
|
if (parsed == null && text != null) {
|
||||||
|
@ -504,7 +515,7 @@ export class CoreFileProvider {
|
||||||
return parsed;
|
return parsed;
|
||||||
});
|
});
|
||||||
default:
|
default:
|
||||||
return File.readAsText(this.basePath, path);
|
return File.readAsText(folder, path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1235,7 +1246,7 @@ export class CoreFileProvider {
|
||||||
* @return Path.
|
* @return Path.
|
||||||
*/
|
*/
|
||||||
getWWWAbsolutePath(): string {
|
getWWWAbsolutePath(): string {
|
||||||
if (cordova && cordova.file && cordova.file.applicationDirectory) {
|
if (window.cordova && cordova.file && cordova.file.applicationDirectory) {
|
||||||
return CoreTextUtils.concatenatePaths(cordova.file.applicationDirectory, 'www');
|
return CoreTextUtils.concatenatePaths(cordova.file.applicationDirectory, 'www');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue