commit
8400bf7452
|
@ -0,0 +1,55 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CoreCronDelegate } from '@providers/cron';
|
||||
import { CoreContentLinksDelegate } from '@core/contentlinks/providers/delegate';
|
||||
import { CoreCourseModuleDelegate } from '@core/course/providers/module-delegate';
|
||||
import { CoreCourseModulePrefetchDelegate } from '@core/course/providers/module-prefetch-delegate';
|
||||
import { AddonModChoiceComponentsModule } from './components/components.module';
|
||||
import { AddonModChoiceModuleHandler } from './providers/module-handler';
|
||||
import { AddonModChoiceProvider } from './providers/choice';
|
||||
import { AddonModChoiceLinkHandler } from './providers/link-handler';
|
||||
import { AddonModChoicePrefetchHandler } from './providers/prefetch-handler';
|
||||
import { AddonModChoiceSyncProvider } from './providers/sync';
|
||||
import { AddonModChoiceSyncCronHandler } from './providers/sync-cron-handler';
|
||||
import { AddonModChoiceOfflineProvider } from './providers/offline';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
],
|
||||
imports: [
|
||||
AddonModChoiceComponentsModule
|
||||
],
|
||||
providers: [
|
||||
AddonModChoiceProvider,
|
||||
AddonModChoiceModuleHandler,
|
||||
AddonModChoicePrefetchHandler,
|
||||
AddonModChoiceLinkHandler,
|
||||
AddonModChoiceSyncCronHandler,
|
||||
AddonModChoiceSyncProvider,
|
||||
AddonModChoiceOfflineProvider
|
||||
]
|
||||
})
|
||||
export class AddonModChoiceModule {
|
||||
constructor(moduleDelegate: CoreCourseModuleDelegate, moduleHandler: AddonModChoiceModuleHandler,
|
||||
prefetchDelegate: CoreCourseModulePrefetchDelegate, prefetchHandler: AddonModChoicePrefetchHandler,
|
||||
contentLinksDelegate: CoreContentLinksDelegate, linkHandler: AddonModChoiceLinkHandler,
|
||||
cronDelegate: CoreCronDelegate, syncHandler: AddonModChoiceSyncCronHandler) {
|
||||
moduleDelegate.registerHandler(moduleHandler);
|
||||
prefetchDelegate.registerHandler(prefetchHandler);
|
||||
contentLinksDelegate.registerHandler(linkHandler);
|
||||
cronDelegate.register(syncHandler);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { IonicModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreComponentsModule } from '@components/components.module';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { CoreCourseComponentsModule } from '@core/course/components/components.module';
|
||||
import { AddonModChoiceIndexComponent } from './index/index';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModChoiceIndexComponent
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
IonicModule,
|
||||
TranslateModule.forChild(),
|
||||
CoreComponentsModule,
|
||||
CoreDirectivesModule,
|
||||
CoreCourseComponentsModule
|
||||
],
|
||||
providers: [
|
||||
],
|
||||
exports: [
|
||||
AddonModChoiceIndexComponent
|
||||
],
|
||||
entryComponents: [
|
||||
AddonModChoiceIndexComponent
|
||||
]
|
||||
})
|
||||
export class AddonModChoiceComponentsModule {}
|
|
@ -0,0 +1,90 @@
|
|||
<!-- Buttons to add to the header. -->
|
||||
<core-navbar-buttons end>
|
||||
<core-context-menu>
|
||||
<core-context-menu-item *ngIf="externalUrl" [priority]="900" [content]="'core.openinbrowser' | translate" [href]="externalUrl" [iconAction]="'open'"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="description" [priority]="800" [content]="'core.moduleintro' | translate" (action)="expandDescription()" [iconAction]="'arrow-forward'"></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" [content]="'core.settings.synchronizenow' | translate" (action)="doRefresh(null, $event, true)" [iconAction]="syncIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="prefetchStatusIcon" [priority]="500" [content]="prefetchText" (action)="prefetch()" [iconAction]="prefetchStatusIcon" [closeOnClick]="false"></core-context-menu-item>
|
||||
<core-context-menu-item *ngIf="size" [priority]="400" [content]="size" [iconDescription]="'cube'" (action)="removeFiles()" [iconAction]="'trash'"></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"></core-course-module-description>
|
||||
|
||||
<!-- Activity availability messages -->
|
||||
<ion-card class="core-info-card" icon-start *ngIf="choiceNotOpenYet">
|
||||
<ion-icon name="information-circle"></ion-icon>
|
||||
<p *ngIf="options && options.length">{{ 'addon.mod_choice.previewonly' | translate:{$a: choice.openTimeReadable} }}</p>
|
||||
<p *ngIf="!options || !options.length">{{ 'addon.mod_choice.notopenyet' | translate:{$a: choice.openTimeReadable} }}</p>
|
||||
</ion-card>
|
||||
<ion-card class="core-info-card" icon-start *ngIf="choiceClosed">
|
||||
<ion-icon name="information-circle"></ion-icon>
|
||||
<p *ngIf="options && options.length">{{ 'addon.mod_choice.yourselection' | translate }} <core-format-text [text]="options[0].text"></core-format-text></p>
|
||||
<p>{{ 'addon.mod_choice.expired' | translate:{$a: choice.closeTimeReadable} }}</p>
|
||||
</ion-card>
|
||||
|
||||
<!-- Choice done in offline but not synchronized -->
|
||||
<ion-card class="core-warning-card" icon-start *ngIf="hasOffline">
|
||||
<ion-icon name="warning"></ion-icon> {{ 'core.hasdatatosync' | translate:{$a: moduleName} }}
|
||||
</ion-card>
|
||||
|
||||
<!-- Choice options -->
|
||||
<ion-card *ngIf="options && options.length">
|
||||
<ng-container *ngIf="choice.allowmultiple">
|
||||
<ion-item text-wrap *ngFor="let option of options">
|
||||
<ion-label><core-format-text [text]="option.text"></core-format-text> <span *ngIf="choice.limitanswers && option.countanswers >= option.maxanswers">{{ 'addon.mod_choice.full' | translate }}</span></ion-label>
|
||||
<ion-checkbox item-end [(ngModel)]="option.checked" [disabled]="option.disabled || !canEdit"></ion-checkbox>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!choice.allowmultiple">
|
||||
<ion-item text-wrap *ngFor="let option of options" radio-group [(ngModel)]="selectedOption.id">
|
||||
<ion-label><core-format-text [text]="option.text"></core-format-text> <span *ngIf="choice.limitanswers && option.countanswers >= option.maxanswers">{{ 'addon.mod_choice.full' | translate }}</span></ion-label>
|
||||
<ion-radio color="primary" [value]="option.id" [disabled]="option.disabled || !canEdit"></ion-radio>
|
||||
</ion-item>
|
||||
</ng-container>
|
||||
<ion-item *ngIf="canEdit">
|
||||
<button ion-button block (click)="save()" [disabled]="!canSave()">{{ 'addon.mod_choice.savemychoice' | translate }}</button>
|
||||
</ion-item>
|
||||
<ion-item *ngIf="canDelete">
|
||||
<button ion-button block color="light" (click)="delete()">{{ 'addon.mod_choice.removemychoice' | translate }}</button>
|
||||
</ion-item>
|
||||
</ion-card>
|
||||
|
||||
<!-- Choice results -->
|
||||
<ion-card *ngIf="canSeeResults">
|
||||
<ion-item-divider color="light" text-center>
|
||||
{{ 'addon.mod_choice.responses' | translate }}
|
||||
</ion-item-divider>
|
||||
<ion-grid no-padding>
|
||||
<ion-row>
|
||||
<ion-col col-12 col-lg-4>
|
||||
<ion-item text-wrap class="core-warning-item" *ngIf="hasOffline">
|
||||
<ion-icon item-start name="warning" color="warning"></ion-icon> {{ 'addon.mod_choice.resultsnotsynced' | translate }}
|
||||
</ion-item>
|
||||
<ion-item>
|
||||
<canvas core-chart type="pie" [data]="data" [labels]="labels"></canvas>
|
||||
</ion-item>
|
||||
</ion-col>
|
||||
<ion-col *ngIf="choice.publish && results" col-12 col-lg-8>
|
||||
<ion-item text-wrap *ngFor="let result of results">
|
||||
<h2><core-format-text [text]="result.text"></core-format-text></h2>
|
||||
<p>{{ 'addon.mod_choice.numberofuser' | translate }}: {{ result.numberofuser }} ({{ 'core.percentagenumber' | translate: {$a: result.percentageamount} }})</p>
|
||||
<a ion-item *ngFor="let user of result.userresponses" core-user-link [courseId]="courseid" [userId]="user.userid" [title]="user.fullname">
|
||||
<ion-avatar item-start>
|
||||
<img [src]="user.profileimageurl" [alt]="'core.pictureof' | translate:{$a: user.fullname}" core-external-content onError="this.src='assets/img/user-avatar.png'">
|
||||
</ion-avatar>
|
||||
<p>{{user.fullname}}</p>
|
||||
</a>
|
||||
</ion-item>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ion-card>
|
||||
<ion-card class="core-info-card" *ngIf="!canSeeResults && !choiceNotOpenYet">
|
||||
<p>{{ 'addon.mod_choice.noresultsviewable' | translate }}</p>
|
||||
</ion-card>
|
||||
</core-loading>
|
|
@ -0,0 +1,378 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector } from '@angular/core';
|
||||
import { Content } from 'ionic-angular';
|
||||
import { CoreCourseModuleMainActivityComponent } from '@core/course/classes/main-activity-component';
|
||||
import { AddonModChoiceProvider } from '../../providers/choice';
|
||||
import { AddonModChoiceOfflineProvider } from '../../providers/offline';
|
||||
import { AddonModChoiceSyncProvider } from '../../providers/sync';
|
||||
import * as moment from 'moment';
|
||||
|
||||
/**
|
||||
* Component that displays a choice.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'addon-mod-choice-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModChoiceIndexComponent extends CoreCourseModuleMainActivityComponent {
|
||||
component = AddonModChoiceProvider.COMPONENT;
|
||||
moduleName = 'choice';
|
||||
|
||||
choice: any;
|
||||
options = [];
|
||||
selectedOption: any;
|
||||
choiceNotOpenYet = false;
|
||||
choiceClosed = false;
|
||||
canEdit = false;
|
||||
canDelete = false;
|
||||
canSeeResults = false;
|
||||
data = [];
|
||||
labels = [];
|
||||
results = [];
|
||||
|
||||
protected userId: number;
|
||||
protected syncEventName = AddonModChoiceSyncProvider.AUTO_SYNCED;
|
||||
protected hasAnsweredOnline = false;
|
||||
protected now: number;
|
||||
|
||||
constructor(injector: Injector, private choiceProvider: AddonModChoiceProvider, @Optional() private content: Content,
|
||||
private choiceOffline: AddonModChoiceOfflineProvider, private choiceSync: AddonModChoiceSyncProvider) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Component being initialized.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
super.ngOnInit();
|
||||
|
||||
this.userId = this.sitesProvider.getCurrentSiteUserId();
|
||||
|
||||
this.loadContent(false, true).then(() => {
|
||||
if (!this.choice) {
|
||||
return;
|
||||
}
|
||||
this.choiceProvider.logView(this.choice.id).then(() => {
|
||||
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||
}).catch((error) => {
|
||||
// Ignore errors.
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the invalidate content function.
|
||||
*
|
||||
* @return {Promise<any>} Resolved when done.
|
||||
*/
|
||||
protected invalidateContent(): Promise<any> {
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.choiceProvider.invalidateChoiceData(this.courseId));
|
||||
|
||||
if (this.choice) {
|
||||
promises.push(this.choiceProvider.invalidateOptions(this.choice.id));
|
||||
promises.push(this.choiceProvider.invalidateResults(this.choice.id));
|
||||
}
|
||||
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares sync event data with current data to check if refresh content is needed.
|
||||
*
|
||||
* @param {any} syncEventData Data receiven on sync observer.
|
||||
* @return {boolean} True if refresh is needed, false otherwise.
|
||||
*/
|
||||
protected isRefreshSyncNeeded(syncEventData: any): boolean {
|
||||
if (this.choice && syncEventData.choiceId == this.choice.id && syncEventData.userId == this.userId) {
|
||||
this.content.scrollToTop();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download choice contents.
|
||||
*
|
||||
* @param {boolean} [refresh=false] If it's refreshing content.
|
||||
* @param {boolean} [sync=false] If the refresh is needs syncing.
|
||||
* @param {boolean} [showErrors=false] If show errors to the user of hide them.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchContent(refresh: boolean = false, sync: boolean = false, showErrors: boolean = false): Promise<any> {
|
||||
this.now = new Date().getTime();
|
||||
|
||||
return this.choiceProvider.getChoice(this.courseId, this.module.id).then((choice) => {
|
||||
this.choice = choice;
|
||||
this.choice.timeopen = parseInt(choice.timeopen) * 1000;
|
||||
this.choice.openTimeReadable = moment(choice.timeopen).format('LLL');
|
||||
this.choice.timeclose = parseInt(choice.timeclose) * 1000;
|
||||
this.choice.closeTimeReadable = moment(choice.timeclose).format('LLL');
|
||||
|
||||
this.description = choice.intro || choice.description;
|
||||
this.choiceNotOpenYet = choice.timeopen && choice.timeopen > this.now;
|
||||
this.choiceClosed = choice.timeclose && choice.timeclose <= this.now;
|
||||
|
||||
this.dataRetrieved.emit(choice);
|
||||
|
||||
if (sync) {
|
||||
// Try to synchronize the choice.
|
||||
return this.syncActivity(showErrors).then((updated) => {
|
||||
if (updated) {
|
||||
// Responses were sent, update the choice.
|
||||
return this.choiceProvider.getChoice(this.courseId, this.module.id).then((choice) => {
|
||||
this.choice = choice;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
// Check if there are responses stored in offline.
|
||||
return this.choiceOffline.hasResponse(this.choice.id);
|
||||
}).then((hasOffline) => {
|
||||
this.hasOffline = hasOffline;
|
||||
|
||||
// We need fetchOptions to finish before calling fetchResults because it needs hasAnsweredOnline variable.
|
||||
return this.fetchOptions(hasOffline).then(() => {
|
||||
return this.fetchResults();
|
||||
});
|
||||
}).then(() => {
|
||||
// All data obtained, now fill the context menu.
|
||||
this.fillContextMenu(refresh);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get choice options.
|
||||
*
|
||||
* @param {boolean} hasOffline True if there are responses stored offline.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected fetchOptions(hasOffline: boolean): Promise<any> {
|
||||
return this.choiceProvider.getOptions(this.choice.id).then((options) => {
|
||||
let promise;
|
||||
|
||||
// Check if the user has answered (synced) to allow show results.
|
||||
this.hasAnsweredOnline = options.some((option) => option.checked);
|
||||
|
||||
if (hasOffline) {
|
||||
promise = this.choiceOffline.getResponse(this.choice.id).then((response) => {
|
||||
const optionsKeys = {};
|
||||
options.forEach((option) => {
|
||||
optionsKeys[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 (optionsKeys[selected] && optionsKeys[selected].checked) {
|
||||
optionsKeys[selected].checked = false;
|
||||
optionsKeys[selected].countanswers--;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// On empty responses, uncheck all selected.
|
||||
Object.keys(optionsKeys).forEach((key) => {
|
||||
if (optionsKeys[key].checked) {
|
||||
optionsKeys[key].checked = false;
|
||||
optionsKeys[key].countanswers--;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// Uncheck all options to check again the offlines'.
|
||||
Object.keys(optionsKeys).forEach((key) => {
|
||||
if (optionsKeys[key].checked) {
|
||||
optionsKeys[key].checked = false;
|
||||
optionsKeys[key].countanswers--;
|
||||
}
|
||||
});
|
||||
// Then check selected ones.
|
||||
response.responses.forEach((selected) => {
|
||||
if (optionsKeys[selected]) {
|
||||
optionsKeys[selected].checked = true;
|
||||
optionsKeys[selected].countanswers++;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Convert it again to array.
|
||||
return Object.keys(optionsKeys).map((key) => optionsKeys[key]);
|
||||
});
|
||||
} else {
|
||||
promise = Promise.resolve(options);
|
||||
}
|
||||
|
||||
promise.then((options) => {
|
||||
const isOpen = this.isChoiceOpen();
|
||||
|
||||
let hasAnswered = false;
|
||||
this.selectedOption = {id: -1}; // Single choice model.
|
||||
options.forEach((option) => {
|
||||
if (option.checked) {
|
||||
hasAnswered = true;
|
||||
if (!this.choice.allowmultiple) {
|
||||
this.selectedOption.id = option.id;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.canEdit = isOpen && (this.choice.allowupdate || !hasAnswered);
|
||||
this.canDelete = isOpen && this.choice.allowupdate && hasAnswered;
|
||||
this.options = options;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience function to get choice results.
|
||||
*
|
||||
* @return {Promise<any>} Resolved when done.
|
||||
*/
|
||||
protected fetchResults(): Promise<any> {
|
||||
if (this.choiceNotOpenYet) {
|
||||
// Cannot see results yet.
|
||||
this.canSeeResults = false;
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return this.choiceProvider.getResults(this.choice.id).then((results) => {
|
||||
let hasVotes = false;
|
||||
this.data = [];
|
||||
this.labels = [];
|
||||
results.forEach((result) => {
|
||||
if (result.numberofuser > 0) {
|
||||
hasVotes = true;
|
||||
}
|
||||
result.percentageamount = parseFloat(result.percentageamount).toFixed(1);
|
||||
this.data.push(result.numberofuser);
|
||||
this.labels.push(result.text);
|
||||
});
|
||||
this.canSeeResults = hasVotes || this.choiceProvider.canStudentSeeResults(this.choice, this.hasAnsweredOnline);
|
||||
this.results = results;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a choice is open.
|
||||
*
|
||||
* @return {boolean} True if choice is open, false otherwise.
|
||||
*/
|
||||
protected isChoiceOpen(): boolean {
|
||||
return (this.choice.timeopen === 0 || this.choice.timeopen <= this.now) &&
|
||||
(this.choice.timeclose === 0 || this.choice.timeclose > this.now);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the user has selected at least one option.
|
||||
*
|
||||
* @return {boolean} True if the user has responded.
|
||||
*/
|
||||
canSave(): boolean {
|
||||
if (this.choice.allowmultiple) {
|
||||
return this.options.some((option) => option.checked);
|
||||
} else {
|
||||
return this.selectedOption.id !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Save options selected.
|
||||
*/
|
||||
save(): void {
|
||||
// Only show confirm if choice doesn't allow update.
|
||||
let promise;
|
||||
if (this.choice.allowupdate) {
|
||||
promise = Promise.resolve();
|
||||
} else {
|
||||
promise = this.domUtils.showConfirm(this.translate.instant('core.areyousure'));
|
||||
}
|
||||
|
||||
promise.then(() => {
|
||||
const responses = [];
|
||||
if (this.choice.allowmultiple) {
|
||||
this.options.forEach((option) => {
|
||||
if (option.checked) {
|
||||
responses.push(option.id);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
responses.push(this.selectedOption.id);
|
||||
}
|
||||
|
||||
const modal = this.domUtils.showModalLoading('core.sending', true);
|
||||
this.choiceProvider.submitResponse(this.choice.id, this.choice.name, this.courseId, responses).then(() => {
|
||||
// Success!
|
||||
// Check completion since it could be configured to complete once the user answers the choice.
|
||||
this.courseProvider.checkModuleCompletion(this.courseId, this.module.completionstatus);
|
||||
this.content.scrollToTop();
|
||||
|
||||
// Let's refresh the data.
|
||||
return this.refreshContent(false);
|
||||
}).catch((message) => {
|
||||
this.domUtils.showErrorModalDefault(message, 'addon.mod_choice.cannotsubmit', true);
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete options selected.
|
||||
*/
|
||||
delete(): void {
|
||||
this.domUtils.showConfirm(this.translate.instant('core.areyousure')).then(() => {
|
||||
const modal = this.domUtils.showModalLoading('core.sending', true);
|
||||
this.choiceProvider.deleteResponses(this.choice.id, this.choice.name, this.courseId).then(() => {
|
||||
this.content.scrollToTop();
|
||||
|
||||
// Success! Let's refresh the data.
|
||||
return this.refreshContent(false);
|
||||
}).catch((message) => {
|
||||
this.domUtils.showErrorModalDefault(message, 'addon.mod_choice.cannotsubmit', true);
|
||||
}).finally(() => {
|
||||
modal.dismiss();
|
||||
});
|
||||
}).catch(() => {
|
||||
// Ingore cancelled modal.
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs the sync of the activity.
|
||||
*
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected sync(): Promise<any> {
|
||||
return this.choiceSync.syncChoice(this.choice.id, this.userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if sync has succeed from result sync data.
|
||||
*
|
||||
* @param {any} result Data returned on the sync function.
|
||||
* @return {boolean} Whether it succeed or not.
|
||||
*/
|
||||
protected hasSyncSucceed(result: any): boolean {
|
||||
return result.updated;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"cannotsubmit": "Sorry, there was a problem submitting your choice. Please try again.",
|
||||
"choiceoptions": "Choice options",
|
||||
"errorgetchoice": "Error getting choice data.",
|
||||
"expired": "Sorry, this activity closed on {{$a}} and is no longer available",
|
||||
"full": "(Full)",
|
||||
"noresultsviewable": "The results are not currently viewable.",
|
||||
"notopenyet": "Sorry, 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}}.",
|
||||
"removemychoice": "Remove my choice",
|
||||
"responses": "Responses",
|
||||
"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,16 @@
|
|||
<ion-header>
|
||||
<ion-navbar>
|
||||
<ion-title><core-format-text [text]="title"></core-format-text></ion-title>
|
||||
|
||||
<ion-buttons end>
|
||||
<!-- The buttons defined by the component will be added in here. -->
|
||||
</ion-buttons>
|
||||
</ion-navbar>
|
||||
</ion-header>
|
||||
<ion-content>
|
||||
<ion-refresher [enabled]="choiceComponent.loaded" (ionRefresh)="choiceComponent.doRefresh($event)">
|
||||
<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,33 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { NgModule } from '@angular/core';
|
||||
import { IonicPageModule } from 'ionic-angular';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CoreDirectivesModule } from '@directives/directives.module';
|
||||
import { AddonModChoiceComponentsModule } from '../../components/components.module';
|
||||
import { AddonModChoiceIndexPage } from './index';
|
||||
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AddonModChoiceIndexPage,
|
||||
],
|
||||
imports: [
|
||||
CoreDirectivesModule,
|
||||
AddonModChoiceComponentsModule,
|
||||
IonicPageModule.forChild(AddonModChoiceIndexPage),
|
||||
TranslateModule.forChild()
|
||||
],
|
||||
})
|
||||
export class AddonModChoiceIndexPageModule {}
|
|
@ -0,0 +1,48 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { IonicPage, NavParams } from 'ionic-angular';
|
||||
import { AddonModChoiceIndexComponent } from '../../components/index/index';
|
||||
|
||||
/**
|
||||
* Page that displays a choice.
|
||||
*/
|
||||
@IonicPage({ segment: 'addon-mod-choice-index' })
|
||||
@Component({
|
||||
selector: 'page-addon-mod-choice-index',
|
||||
templateUrl: 'index.html',
|
||||
})
|
||||
export class AddonModChoiceIndexPage {
|
||||
@ViewChild(AddonModChoiceIndexComponent) choiceComponent: AddonModChoiceIndexComponent;
|
||||
|
||||
title: string;
|
||||
module: any;
|
||||
courseId: number;
|
||||
|
||||
constructor(navParams: NavParams) {
|
||||
this.module = navParams.get('module') || {};
|
||||
this.courseId = navParams.get('courseId');
|
||||
this.title = this.module.name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update some data based on the choice instance.
|
||||
*
|
||||
* @param {any} choice Choice instance.
|
||||
*/
|
||||
updateData(choice: any): void {
|
||||
this.title = choice.name || this.title;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,425 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreFilepoolProvider } from '@providers/filepool';
|
||||
import { AddonModChoiceOfflineProvider } from './offline';
|
||||
|
||||
/**
|
||||
* Service that provides some features for choices.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModChoiceProvider {
|
||||
static COMPONENT = 'mmaModChoice';
|
||||
|
||||
static RESULTS_NOT = 0;
|
||||
static RESULTS_AFTER_ANSWER = 1;
|
||||
static RESULTS_AFTER_CLOSE = 2;
|
||||
static RESULTS_ALWAYS = 3;
|
||||
|
||||
protected ROOT_CACHE_KEY = 'mmaModChoice:';
|
||||
|
||||
constructor(private sitesProvider: CoreSitesProvider, private appProvider: CoreAppProvider,
|
||||
private filepoolProvider: CoreFilepoolProvider, private utils: CoreUtilsProvider,
|
||||
private choiceOffline: AddonModChoiceOfflineProvider) {}
|
||||
|
||||
/**
|
||||
* 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 {any} choice Choice to check.
|
||||
* @param {boolean} hasAnswered True if user has answered the choice, false otherwise.
|
||||
* @return {boolean} True if the students can see the results.
|
||||
*/
|
||||
canStudentSeeResults(choice: any, hasAnswered: boolean): boolean {
|
||||
const now = new Date().getTime();
|
||||
|
||||
return choice.showresults === AddonModChoiceProvider.RESULTS_ALWAYS ||
|
||||
choice.showresults === AddonModChoiceProvider.RESULTS_AFTER_CLOSE &&
|
||||
choice.timeclose !== 0 && choice.timeclose <= now ||
|
||||
choice.showresults === AddonModChoiceProvider.RESULTS_AFTER_ANSWER && hasAnswered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete responses from a choice.
|
||||
*
|
||||
* @param {number} choiceId Choice ID.
|
||||
* @param {string} name Choice name.
|
||||
* @param {number} courseId Course ID the choice belongs to.
|
||||
* @param {number[]} [responses] IDs of the answers. If not defined, delete all the answers of the current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the options are deleted.
|
||||
*/
|
||||
deleteResponses(choiceId: number, name: string, courseId: number, responses?: number[], siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
responses = responses || [];
|
||||
|
||||
// Convenience function to store a message to be synchronized later.
|
||||
const storeOffline = (): Promise<any> => {
|
||||
return this.choiceOffline.saveResponse(choiceId, name, courseId, responses, true, siteId).then(() => {
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
if (!this.appProvider.isOnline()) {
|
||||
// App is offline, store the action.
|
||||
return storeOffline();
|
||||
}
|
||||
|
||||
// If there's already a response to be sent to the server, discard it first.
|
||||
return this.choiceOffline.deleteResponse(choiceId, siteId).then(() => {
|
||||
return this.deleteResponsesOnline(choiceId, responses, siteId).then(() => {
|
||||
return true;
|
||||
}).catch((error) => {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||
return Promise.reject(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 {number} choiceId Choice ID.
|
||||
* @param {number[]} [responses] IDs of the answers. If not defined, delete all the answers of the current user.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when responses are successfully deleted.
|
||||
*/
|
||||
deleteResponsesOnline(choiceId: number, responses?: number[], siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
choiceid: choiceId,
|
||||
responses: responses
|
||||
};
|
||||
|
||||
return site.write('mod_choice_delete_choice_responses', params).then((response) => {
|
||||
// Other errors ocurring.
|
||||
if (!response || response.status === false) {
|
||||
return Promise.reject(this.utils.createFakeWSError(''));
|
||||
}
|
||||
|
||||
// Invalidate related data.
|
||||
const promises = [
|
||||
this.invalidateOptions(choiceId, site.id),
|
||||
this.invalidateResults(choiceId, site.id)
|
||||
];
|
||||
|
||||
return Promise.all(promises).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for choice data WS calls.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getChoiceDataCacheKey(courseId: number): string {
|
||||
return this.ROOT_CACHE_KEY + 'choice:' + courseId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for choice options WS calls.
|
||||
*
|
||||
* @param {number} choiceId Choice ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getChoiceOptionsCacheKey(choiceId: number): string {
|
||||
return this.ROOT_CACHE_KEY + 'options:' + choiceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cache key for choice results WS calls.
|
||||
*
|
||||
* @param {number} choiceId Choice ID.
|
||||
* @return {string} Cache key.
|
||||
*/
|
||||
protected getChoiceResultsCacheKey(choiceId: number): string {
|
||||
return this.ROOT_CACHE_KEY + 'results:' + choiceId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a choice with key=value. If more than one is found, only the first will be returned.
|
||||
*
|
||||
* @param {string} siteId Site ID.
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {string} key Name of the property to check.
|
||||
* @param {any} value Value to search.
|
||||
* @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
|
||||
* @return {Promise<any>} Promise resolved when the choice is retrieved.
|
||||
*/
|
||||
protected getChoiceByDataKey(siteId: string, courseId: number, key: string, value: any, forceCache: boolean = false)
|
||||
: Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
courseids: [courseId]
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getChoiceDataCacheKey(courseId),
|
||||
omitExpires: forceCache
|
||||
};
|
||||
|
||||
return site.read('mod_choice_get_choices_by_courses', params, preSets).then((response) => {
|
||||
if (response && response.choices) {
|
||||
const currentChoice = response.choices.find((choice) => choice[key] == value);
|
||||
if (currentChoice) {
|
||||
return currentChoice;
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a choice by course module ID.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {number} cmId Course module ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
|
||||
* @return {Promise<any>} Promise resolved when the choice is retrieved.
|
||||
*/
|
||||
getChoice(courseId: number, cmId: number, siteId?: string, forceCache: boolean = false): Promise<any> {
|
||||
return this.getChoiceByDataKey(siteId, courseId, 'coursemodule', cmId, forceCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a choice by ID.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {number} choiceId Choice ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {boolean} [forceCache=false] True to always get the value from cache, false otherwise. Default false.
|
||||
* @return {Promise<any>} Promise resolved when the choice is retrieved.
|
||||
*/
|
||||
getChoiceById(courseId: number, choiceId: number, siteId?: string, forceCache: boolean = false): Promise<any> {
|
||||
return this.getChoiceByDataKey(siteId, courseId, 'id', choiceId, forceCache);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get choice options.
|
||||
*
|
||||
* @param {number} choiceId Choice ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with choice options.
|
||||
*/
|
||||
getOptions(choiceId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
choiceid: choiceId
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getChoiceOptionsCacheKey(choiceId)
|
||||
};
|
||||
|
||||
return site.read('mod_choice_get_choice_options', params, preSets).then((response) => {
|
||||
if (response.options) {
|
||||
return response.options;
|
||||
}
|
||||
|
||||
return Promise.reject(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get choice results.
|
||||
*
|
||||
* @param {number} choiceId Choice ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved with choice results.
|
||||
*/
|
||||
getResults(choiceId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
choiceid: choiceId
|
||||
};
|
||||
const preSets = {
|
||||
cacheKey: this.getChoiceResultsCacheKey(choiceId)
|
||||
};
|
||||
|
||||
return site.read('mod_choice_get_choice_results', params, preSets).then((response) => {
|
||||
if (response.options) {
|
||||
return response.options;
|
||||
}
|
||||
|
||||
return Promise.reject(null);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate choice data.
|
||||
*
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateChoiceData(courseId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(null).then((site) => {
|
||||
return site.invalidateWsCacheForKey(this.getChoiceDataCacheKey(courseId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the prefetched content.
|
||||
*
|
||||
* @param {number} moduleId The module ID.
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when data is invalidated.
|
||||
*/
|
||||
invalidateContent(moduleId: number, courseId: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
const promises = [];
|
||||
|
||||
promises.push(this.getChoice(courseId, moduleId).then((choice) => {
|
||||
return Promise.all([
|
||||
this.invalidateChoiceData(courseId),
|
||||
this.invalidateOptions(choice.id),
|
||||
this.invalidateResults(choice.id),
|
||||
]);
|
||||
}));
|
||||
|
||||
promises.push(this.filepoolProvider.invalidateFilesByComponent(siteId, AddonModChoiceProvider.COMPONENT, moduleId));
|
||||
|
||||
return this.utils.allPromises(promises);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate choice options.
|
||||
*
|
||||
* @param {number} choiceId Choice ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateOptions(choiceId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKey(this.getChoiceOptionsCacheKey(choiceId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate choice results.
|
||||
*
|
||||
* @param {number} choiceId Choice ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateResults(choiceId: number, siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.invalidateWsCacheForKey(this.getChoiceResultsCacheKey(choiceId));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Report the choice as being viewed.
|
||||
*
|
||||
* @param {string} id Choice ID.
|
||||
* @return {Promise<any>} Promise resolved when the WS call is successful.
|
||||
*/
|
||||
logView(id: string): Promise<any> {
|
||||
const params = {
|
||||
choiceid: id
|
||||
};
|
||||
|
||||
return this.sitesProvider.getCurrentSite().write('mod_choice_view_choice', params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a response to a choice to Moodle.
|
||||
*
|
||||
* @param {number} choiceId Choice ID.
|
||||
* @param {string} name Choice name.
|
||||
* @param {number} courseId Course ID the choice belongs to.
|
||||
* @param {number[]} responses IDs of selected options.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<boolean>} Promise resolved with boolean: true if response was sent to server, false if stored in device.
|
||||
*/
|
||||
submitResponse(choiceId: number, name: string, courseId: number, responses: number[], siteId?: string): Promise<boolean> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
// Convenience function to store a message to be synchronized later.
|
||||
const storeOffline = (): Promise<any> => {
|
||||
return this.choiceOffline.saveResponse(choiceId, name, courseId, responses, false, siteId).then(() => {
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
if (!this.appProvider.isOnline()) {
|
||||
// App is offline, store the action.
|
||||
return storeOffline();
|
||||
}
|
||||
|
||||
// If there's already a response to be sent to the server, discard it first.
|
||||
return this.choiceOffline.deleteResponse(choiceId, siteId).then(() => {
|
||||
return this.submitResponseOnline(choiceId, responses, siteId).then(() => {
|
||||
return true;
|
||||
}).catch((error) => {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted.
|
||||
return Promise.reject(error);
|
||||
} else {
|
||||
// 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 {number} choiceId Choice ID.
|
||||
* @param {number[]} responses IDs of selected options.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved when responses are successfully submitted.
|
||||
*/
|
||||
submitResponseOnline(choiceId: number, responses: number[], siteId?: string): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const params = {
|
||||
choiceid: choiceId,
|
||||
responses: responses
|
||||
};
|
||||
|
||||
return site.write('mod_choice_submit_choice_response', params).then(() => {
|
||||
// Invalidate related data.
|
||||
const promises = [
|
||||
this.invalidateOptions(choiceId, siteId),
|
||||
this.invalidateResults(choiceId, siteId)
|
||||
];
|
||||
|
||||
return Promise.all(promises).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 '@core/contentlinks/classes/module-index-handler';
|
||||
import { CoreCourseHelperProvider } from '@core/course/providers/helper';
|
||||
|
||||
/**
|
||||
* Handler to treat links to choice.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModChoiceLinkHandler extends CoreContentLinksModuleIndexHandler {
|
||||
name = 'AddonModChoiceLinkHandler';
|
||||
|
||||
constructor(courseHelper: CoreCourseHelperProvider) {
|
||||
super(courseHelper, 'AddonModChoice', 'choice');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { NavController, NavOptions } from 'ionic-angular';
|
||||
import { AddonModChoiceIndexComponent } from '../components/index/index';
|
||||
import { CoreCourseModuleHandler, CoreCourseModuleHandlerData } from '@core/course/providers/module-delegate';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
|
||||
/**
|
||||
* Handler to support choice modules.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModChoiceModuleHandler implements CoreCourseModuleHandler {
|
||||
name = 'AddonModChoice';
|
||||
modName = 'choice';
|
||||
|
||||
constructor(private courseProvider: CoreCourseProvider) { }
|
||||
|
||||
/**
|
||||
* Check if the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean} Whether or not the handler is enabled on a site level.
|
||||
*/
|
||||
isEnabled(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the data required to display the module in the course contents view.
|
||||
*
|
||||
* @param {any} module The module object.
|
||||
* @param {number} courseId The course ID.
|
||||
* @param {number} sectionId The section ID.
|
||||
* @return {CoreCourseModuleHandlerData} Data to render the module.
|
||||
*/
|
||||
getData(module: any, courseId: number, sectionId: number): CoreCourseModuleHandlerData {
|
||||
return {
|
||||
icon: this.courseProvider.getModuleIconSrc('choice'),
|
||||
title: module.name,
|
||||
class: 'addon-mod_choice-handler',
|
||||
showDownloadButton: true,
|
||||
action(event: Event, navCtrl: NavController, module: any, courseId: number, options: NavOptions): void {
|
||||
navCtrl.push('AddonModChoiceIndexPage', {module: module, courseId: courseId}, options);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the component to render the module. This is needed to support singleactivity course format.
|
||||
* The component returned must implement CoreCourseModuleMainComponent.
|
||||
*
|
||||
* @param {any} course The course object.
|
||||
* @param {any} module The module object.
|
||||
* @return {any} The component to use, undefined if not found.
|
||||
*/
|
||||
getMainComponent(course: any, module: any): any {
|
||||
return AddonModChoiceIndexComponent;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,166 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { CoreSitesProvider } from '@providers/sites';
|
||||
|
||||
/**
|
||||
* Service to handle offline choices.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModChoiceOfflineProvider {
|
||||
|
||||
// Variables for database.
|
||||
protected CHOICE_TABLE = 'addon_mod_choice_responses';
|
||||
protected tablesSchema = [
|
||||
{
|
||||
name: this.CHOICE_TABLE,
|
||||
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']
|
||||
}
|
||||
];
|
||||
|
||||
constructor(private sitesProvider: CoreSitesProvider) {
|
||||
this.sitesProvider.createTablesFromSchema(this.tablesSchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a response.
|
||||
*
|
||||
* @param {number} choiceId Choice ID to remove.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {number} [userId] User the responses belong to. If not defined, current user in site.
|
||||
* @return {Promise<any>} Promise resolved if stored, rejected if failure.
|
||||
*/
|
||||
deleteResponse(choiceId: number, siteId?: string, userId?: number): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
return site.getDb().deleteRecords(this.CHOICE_TABLE, {choiceid: choiceId, userid: userId});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all offline responses.
|
||||
*
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any[]>} Promi[se resolved with responses.
|
||||
*/
|
||||
getResponses(siteId?: string): Promise<any[]> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
return site.getDb().getRecords(this.CHOICE_TABLE).then((records) => {
|
||||
records.forEach((record) => {
|
||||
record.responses = JSON.parse(record.responses);
|
||||
});
|
||||
|
||||
return records;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there are offline responses to send.
|
||||
*
|
||||
* @param {number} choiceId Choice ID.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {number} [userId] User the responses belong to. If not defined, current user in site.
|
||||
* @return {Promise<boolean>} Promise resolved with boolean: true if has offline answers, false otherwise.
|
||||
*/
|
||||
hasResponse(choiceId: number, siteId?: string, userId?: number): Promise<boolean> {
|
||||
return this.getResponse(choiceId, siteId, userId).then((response) => {
|
||||
return !!response.choiceid;
|
||||
}).catch((error) => {
|
||||
// No offline data found, return false.
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get response to be synced.
|
||||
*
|
||||
* @param {number} choiceId Choice ID to get.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {number} [userId] User the responses belong to. If not defined, current user in site.
|
||||
* @return {Promise<any>} Promise resolved with the object to be synced.
|
||||
*/
|
||||
getResponse(choiceId: number, siteId?: string, userId?: number): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
userId = userId || site.getUserId();
|
||||
|
||||
return site.getDb().getRecord(this.CHOICE_TABLE, {choiceid: choiceId, userid: userId}).then((record) => {
|
||||
record.responses = JSON.parse(record.responses);
|
||||
|
||||
return record;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Offline version for sending a response to a choice to Moodle.
|
||||
*
|
||||
* @param {number} choiceId Choice ID.
|
||||
* @param {string} name Choice name.
|
||||
* @param {number} courseId Course ID the choice belongs to.
|
||||
* @param {number[]} responses IDs of selected options.
|
||||
* @param {boolean} deleting If true, the user is deleting responses, if false, submitting.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @param {number} [userId] User the responses belong to. If not defined, current user in site.
|
||||
* @return {Promise<any>} Promise resolved when results are successfully submitted.
|
||||
*/
|
||||
saveResponse(choiceId: number, name: string, courseId: number, responses: number[], deleting: boolean,
|
||||
siteId?: string, userId?: number): Promise<any> {
|
||||
return this.sitesProvider.getSite(siteId).then((site) => {
|
||||
const entry = {
|
||||
choiceid: choiceId,
|
||||
name: name,
|
||||
courseid: courseId,
|
||||
userid: userId || site.getUserId(),
|
||||
responses: JSON.stringify(responses),
|
||||
deleting: deleting ? 1 : 0,
|
||||
timecreated: new Date().getTime()
|
||||
};
|
||||
|
||||
return site.getDb().insertRecord(this.CHOICE_TABLE, entry);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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, Injector } from '@angular/core';
|
||||
import { CoreCourseModulePrefetchHandlerBase } from '@core/course/classes/module-prefetch-handler';
|
||||
import { CoreUserProvider } from '@core/user/providers/user';
|
||||
import { AddonModChoiceProvider } from './choice';
|
||||
import { AddonModChoiceSyncProvider } from './sync';
|
||||
|
||||
/**
|
||||
* Handler to prefetch choices.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModChoicePrefetchHandler extends CoreCourseModulePrefetchHandlerBase {
|
||||
name = 'AddonModChoice';
|
||||
modName = 'choice';
|
||||
component = AddonModChoiceProvider.COMPONENT;
|
||||
updatesNames = /^configuration$|^.*files$|^answers$/;
|
||||
|
||||
constructor(protected injector: Injector, protected choiceProvider: AddonModChoiceProvider,
|
||||
protected syncProvider: AddonModChoiceSyncProvider, protected userProvider: CoreUserProvider) {
|
||||
super(injector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the module.
|
||||
*
|
||||
* @param {any} module The module object returned by WS.
|
||||
* @param {number} courseId Course ID.
|
||||
* @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch.
|
||||
* @return {Promise<any>} Promise resolved when all content is downloaded.
|
||||
*/
|
||||
download(module: any, courseId: number, dirPath?: string): Promise<any> {
|
||||
// Same implementation for download or prefetch.
|
||||
return this.prefetch(module, courseId, false, dirPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a module.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @param {boolean} [single] True if we're downloading a single module, false if we're downloading a whole section.
|
||||
* @param {string} [dirPath] Path of the directory where to store all the content files. @see downloadOrPrefetch.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
prefetch(module: any, courseId?: number, single?: boolean, dirPath?: string): Promise<any> {
|
||||
return this.prefetchPackage(module, courseId, single, this.prefetchChoice.bind(this));
|
||||
}
|
||||
|
||||
/**
|
||||
* Prefetch a choice.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @param {boolean} single True if we're downloading a single module, false if we're downloading a whole section.
|
||||
* @param {String} siteId Site ID.
|
||||
* @return {Promise<any>} Promise resolved when done.
|
||||
*/
|
||||
protected prefetchChoice(module: any, courseId: number, single: boolean, siteId: string): Promise<any> {
|
||||
return this.choiceProvider.getChoice(courseId, module.id, siteId).then((choice) => {
|
||||
const promises = [];
|
||||
|
||||
// Get the options and results.
|
||||
promises.push(this.choiceProvider.getOptions(choice.id, siteId));
|
||||
promises.push(this.choiceProvider.getResults(choice.id, siteId).then((options) => {
|
||||
// If we can see the users that answered, prefetch their profile and avatar.
|
||||
const subPromises = [];
|
||||
options.forEach((option) => {
|
||||
option.userresponses.forEach((response) => {
|
||||
if (response.userid) {
|
||||
subPromises.push(this.userProvider.getProfile(response.userid, courseId, false, siteId));
|
||||
}
|
||||
if (response.profileimageurl) {
|
||||
subPromises.push(this.filepoolProvider.addToQueueByUrl(siteId, response.profileimageurl).catch(() => {
|
||||
// Ignore failures.
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return Promise.all(subPromises);
|
||||
}));
|
||||
|
||||
// Get the intro files.
|
||||
const introFiles = this.getIntroFilesFromInstance(module, choice);
|
||||
promises.push(this.filepoolProvider.addFilesToQueue(siteId, introFiles, AddonModChoiceProvider.COMPONENT, module.id));
|
||||
|
||||
return Promise.all(promises);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns choice intro files.
|
||||
*
|
||||
* @param {any} module The module object returned by WS.
|
||||
* @param {number} courseId Course ID.
|
||||
* @return {Promise<any[]>} Promise resolved with list of intro files.
|
||||
*/
|
||||
getIntroFiles(module: any, courseId: number): Promise<any[]> {
|
||||
return this.choiceProvider.getChoice(courseId, module.id).catch(() => {
|
||||
// Not found, return undefined so module description is used.
|
||||
}).then((choice) => {
|
||||
return this.getIntroFilesFromInstance(module, choice);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate the prefetched content.
|
||||
*
|
||||
* @param {number} moduleId The module ID.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @return {Promise<any>} Promise resolved when the data is invalidated.
|
||||
*/
|
||||
invalidateContent(moduleId: number, courseId: number): Promise<any> {
|
||||
return this.choiceProvider.invalidateContent(moduleId, courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate WS calls needed to determine module status.
|
||||
*
|
||||
* @param {any} module Module.
|
||||
* @param {number} courseId Course ID the module belongs to.
|
||||
* @return {Promise<any>} Promise resolved when invalidated.
|
||||
*/
|
||||
invalidateModule(module: any, courseId: number): Promise<any> {
|
||||
return this.choiceProvider.invalidateChoiceData(courseId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the handler is enabled on a site level.
|
||||
*
|
||||
* @return {boolean|Promise<boolean>} A boolean, or a promise resolved with a boolean, indicating if the handler is enabled.
|
||||
*/
|
||||
isEnabled(): boolean | Promise<boolean> {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 '@providers/cron';
|
||||
import { AddonModChoiceSyncProvider } from './sync';
|
||||
|
||||
/**
|
||||
* Synchronization cron handler.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModChoiceSyncCronHandler implements CoreCronHandler {
|
||||
name = 'AddonModChoiceSyncCronHandler';
|
||||
|
||||
constructor(private choiceSync: AddonModChoiceSyncProvider) {}
|
||||
|
||||
/**
|
||||
* Execute the process.
|
||||
* Receives the ID of the site affected, undefined for all sites.
|
||||
*
|
||||
* @param {string} [siteId] ID of the site affected, undefined for all sites.
|
||||
* @return {Promise<any>} Promise resolved when done, rejected if failure.
|
||||
*/
|
||||
execute(siteId?: string): Promise<any> {
|
||||
return this.choiceSync.syncAllChoices(siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the time between consecutive executions.
|
||||
*
|
||||
* @return {number} Time between consecutive executions (in ms).
|
||||
*/
|
||||
getInterval(): number {
|
||||
return 600000; // 10 minutes.
|
||||
}
|
||||
}
|
|
@ -0,0 +1,194 @@
|
|||
// (C) Copyright 2015 Martin Dougiamas
|
||||
//
|
||||
// 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 { CoreLoggerProvider } from '@providers/logger';
|
||||
import { CoreSyncBaseProvider } from '@classes/base-sync';
|
||||
import { CoreSitesProvider } from '@providers/sites';
|
||||
import { CoreAppProvider } from '@providers/app';
|
||||
import { CoreUtilsProvider } from '@providers/utils/utils';
|
||||
import { CoreTextUtilsProvider } from '@providers/utils/text';
|
||||
import { AddonModChoiceOfflineProvider } from './offline';
|
||||
import { AddonModChoiceProvider } from './choice';
|
||||
import { CoreEventsProvider } from '@providers/events';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { CoreCourseProvider } from '@core/course/providers/course';
|
||||
import { CoreSyncProvider } from '@providers/sync';
|
||||
|
||||
/**
|
||||
* Service to sync choices.
|
||||
*/
|
||||
@Injectable()
|
||||
export class AddonModChoiceSyncProvider extends CoreSyncBaseProvider {
|
||||
|
||||
static AUTO_SYNCED = 'addon_mod_choice_autom_synced';
|
||||
protected componentTranslate: string;
|
||||
|
||||
constructor(protected sitesProvider: CoreSitesProvider, loggerProvider: CoreLoggerProvider,
|
||||
protected appProvider: CoreAppProvider, private choiceOffline: AddonModChoiceOfflineProvider,
|
||||
private eventsProvider: CoreEventsProvider, private choiceProvider: AddonModChoiceProvider,
|
||||
translate: TranslateService, private utils: CoreUtilsProvider, protected textUtils: CoreTextUtilsProvider,
|
||||
courseProvider: CoreCourseProvider, syncProvider: CoreSyncProvider) {
|
||||
super('AddonModChoiceSyncProvider', loggerProvider, sitesProvider, appProvider, syncProvider, textUtils, translate);
|
||||
this.componentTranslate = courseProvider.translateModuleName('choice');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ID of a choice sync.
|
||||
*
|
||||
* @param {number} choiceId Choice ID.
|
||||
* @param {number} userId User the responses belong to.
|
||||
* @return {string} 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 {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
syncAllChoices(siteId?: string): Promise<any> {
|
||||
return this.syncOnSites('choices', this.syncAllChoicesFunc.bind(this), undefined, siteId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync all pending choices on a site.
|
||||
*
|
||||
* @param {string} [siteId] Site ID to sync. If not defined, sync all sites.
|
||||
* @return {Promise<any>} Promise resolved if sync is successful, rejected if sync fails.
|
||||
*/
|
||||
protected syncAllChoicesFunc(siteId?: string): Promise<any> {
|
||||
return this.choiceOffline.getResponses(siteId).then((responses) => {
|
||||
const promises = [];
|
||||
|
||||
// Sync all responses.
|
||||
responses.forEach((response) => {
|
||||
promises.push(this.syncChoice(response.choiceid, response.userid, siteId).then((result) => {
|
||||
if (result && result.updated) {
|
||||
// Sync successful, send event.
|
||||
this.eventsProvider.trigger(AddonModChoiceSyncProvider.AUTO_SYNCED, {
|
||||
choiceId: response.choiceid,
|
||||
userId: response.userid,
|
||||
warnings: result.warnings
|
||||
}, siteId);
|
||||
}
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronize a choice.
|
||||
*
|
||||
* @param {number} choiceId Choice ID to be synced.
|
||||
* @param {number} userId User the answers belong to.
|
||||
* @param {string} [siteId] Site ID. If not defined, current site.
|
||||
* @return {Promise<any>} Promise resolved if sync is successful, rejected otherwise.
|
||||
*/
|
||||
syncChoice(choiceId: number, userId: number, siteId?: string): Promise<any> {
|
||||
siteId = siteId || this.sitesProvider.getCurrentSiteId();
|
||||
|
||||
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}'`);
|
||||
|
||||
let courseId;
|
||||
const result = {
|
||||
warnings: [],
|
||||
updated: false
|
||||
};
|
||||
|
||||
// Get offline responses to be sent.
|
||||
const syncPromise = this.choiceOffline.getResponse(choiceId, siteId, userId).catch(() => {
|
||||
// No offline data found, return empty object.
|
||||
return {};
|
||||
}).then((data) => {
|
||||
if (!data.choiceid) {
|
||||
// Nothing to sync.
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.appProvider.isOnline()) {
|
||||
// Cannot sync in offline.
|
||||
return Promise.reject(null);
|
||||
}
|
||||
|
||||
courseId = data.courseid;
|
||||
|
||||
// Send the responses.
|
||||
let promise;
|
||||
|
||||
if (data.deleting) {
|
||||
// A user has deleted some responses.
|
||||
promise = this.choiceProvider.deleteResponsesOnline(choiceId, data.responses, siteId);
|
||||
} else {
|
||||
// A user has added some responses.
|
||||
promise = this.choiceProvider.submitResponseOnline(choiceId, data.responses, siteId);
|
||||
}
|
||||
|
||||
return promise.then(() => {
|
||||
result.updated = true;
|
||||
|
||||
return this.choiceOffline.deleteResponse(choiceId, siteId, userId);
|
||||
}).catch((error) => {
|
||||
if (this.utils.isWebServiceError(error)) {
|
||||
// The WebService has thrown an error, this means that responses cannot be submitted. Delete them.
|
||||
result.updated = true;
|
||||
|
||||
return this.choiceOffline.deleteResponse(choiceId, siteId, userId).then(() => {
|
||||
// Responses deleted, add a warning.
|
||||
result.warnings.push(this.translate.instant('core.warningofflinedatadeleted', {
|
||||
component: this.componentTranslate,
|
||||
name: data.name,
|
||||
error: error.error
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
// Couldn't connect to server, reject.
|
||||
return Promise.reject(error);
|
||||
});
|
||||
}).then(() => {
|
||||
if (courseId) {
|
||||
const promises = [
|
||||
this.choiceProvider.invalidateChoiceData(courseId),
|
||||
choiceId ? this.choiceProvider.invalidateOptions(choiceId) : Promise.resolve(),
|
||||
choiceId ? this.choiceProvider.invalidateResults(choiceId) : Promise.resolve(),
|
||||
];
|
||||
|
||||
// Data has been sent to server, update choice data.
|
||||
return Promise.all(promises).then(() => {
|
||||
return this.choiceProvider.getChoiceById(courseId, choiceId, siteId);
|
||||
}).catch(() => {
|
||||
// Ignore errors.
|
||||
});
|
||||
}
|
||||
}).then(() => {
|
||||
// Sync finished, set sync time.
|
||||
return this.setSyncTime(syncId, siteId);
|
||||
}).then(() => {
|
||||
// All done, return the warnings.
|
||||
return result;
|
||||
});
|
||||
|
||||
return this.addOngoingSync(syncId, syncPromise, siteId);
|
||||
}
|
||||
}
|
|
@ -131,14 +131,14 @@ export class AddonModSurveySyncProvider extends CoreSyncBaseProvider {
|
|||
return this.getOngoingSync(syncId, siteId);
|
||||
}
|
||||
|
||||
this.logger.debug(`Try to sync survey '${surveyId}' for user '${userId}'`);
|
||||
|
||||
let courseId;
|
||||
const result = {
|
||||
warnings: [],
|
||||
answersSent: false
|
||||
};
|
||||
|
||||
this.logger.debug(`Try to sync survey '${surveyId}' for user '${userId}'`);
|
||||
|
||||
// Get answers to be sent.
|
||||
const syncPromise = this.surveyOffline.getSurveyData(surveyId, siteId, userId).catch(() => {
|
||||
// No offline data found, return empty object.
|
||||
|
|
|
@ -78,6 +78,7 @@ import { AddonUserProfileFieldModule } from '@addon/userprofilefield/userprofile
|
|||
import { AddonFilesModule } from '@addon/files/files.module';
|
||||
import { AddonModBookModule } from '@addon/mod/book/book.module';
|
||||
import { AddonModChatModule } from '@addon/mod/chat/chat.module';
|
||||
import { AddonModChoiceModule } from '@addon/mod/choice/choice.module';
|
||||
import { AddonModLabelModule } from '@addon/mod/label/label.module';
|
||||
import { AddonModResourceModule } from '@addon/mod/resource/resource.module';
|
||||
import { AddonModFeedbackModule } from '@addon/mod/feedback/feedback.module';
|
||||
|
@ -175,6 +176,7 @@ export const CORE_PROVIDERS: any[] = [
|
|||
AddonFilesModule,
|
||||
AddonModBookModule,
|
||||
AddonModChatModule,
|
||||
AddonModChoiceModule,
|
||||
AddonModLabelModule,
|
||||
AddonModResourceModule,
|
||||
AddonModFeedbackModule,
|
||||
|
|
Loading…
Reference in New Issue